Edge Rewrite
Jump to content

Module:BENGALIDATE/sandbox

From Wikipedia, the free encyclopedia
local p = {}
local getArgs = require('Module:Arguments').getArgs
local epoch_module = require('Module:Bengali Calendar Epoch Offset')
local timestamp = epoch_module._main
local BENGALI_UNIX_EPOCH = epoch_module.EPOCH

local EPOCH_YEAR = 1432
local SECONDS_PER_DAY = 86400
local DAY_START_OFFSET = 6 * 3600
local DEFAULT_FORMAT = "[[F]] j, Y"

local BN_MONTH = {
    { name = "Boisakh",   link = "Boisakh",                    abbr = "Boi" },
    { name = "Joishtho",  link = "Joishtho",                   abbr = "Joi" },
    { name = "Asharh",    link = "Asharh",                     abbr = "Ash" },
    { name = "Shrabon",   link = "Shrabon",                    abbr = "Shr" },
    { name = "Bhadro",    link = "Bhadro",                     abbr = "Bha" },
    { name = "Ashshin",   link = "Ashshin",                    abbr = "Ash" },
    { name = "Kartik",    link = "Kartik (month)",              abbr = "Kar" },
    { name = "Ogrohayon", link = "Ogrohayon",                  abbr = "Ogr" },
    { name = "Poush",     link = "Poush",                      abbr = "Pou" },
    { name = "Magh",      link = "Magh (Bengali calendar)",    abbr = "Mag" },
    { name = "Falgun",    link = "Falgun",                     abbr = "Fal" },
    { name = "Chaitro",   link = "Chaitro",                    abbr = "Cha" },
}

local BN_WEEKDAY = {
    { name = "Robibar",         abbr = "Rob" },
    { name = "Sombar",          abbr = "Som" },
    { name = "Mongolbar",       abbr = "Mon" },
    { name = "Budhbar",         abbr = "Bud" },
    { name = "Brihoshpotibar",  abbr = "Bri" },
    { name = "Shukrobar",       abbr = "Shu" },
    { name = "Shonibar",        abbr = "Sho" },
}

local MONTH_LENGTHS_NORMAL = { 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 }
local MONTH_LENGTHS_LEAP   = { 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 30 }

local function isLeapYear(year)
    local n = year - 593
    return (n % 400 == 0) or (n % 100 ~= 0 and n % 4 == 0)
end

local function getMonthLengths(year)
    return isLeapYear(year) and MONTH_LENGTHS_LEAP or MONTH_LENGTHS_NORMAL
end

local function daysToDate(days)
    local year = EPOCH_YEAR
    if days >= 0 then
        while true do
            local year_len = isLeapYear(year) and 366 or 365
            if days < year_len then break end
            days = days - year_len
            year = year + 1
        end
    else
        repeat
            year = year - 1
            local year_len = isLeapYear(year) and 366 or 365
            days = days + year_len
        until days >= 0
    end
    local lengths = getMonthLengths(year)
    local month = 1
    local day_of_year = days
    while month < 12 and days >= lengths[month] do
        days = days - lengths[month]
        month = month + 1
    end
    return year, month, days + 1, day_of_year
end

local function tsToWeekday(ts)
    local day_index = math.floor(ts / SECONDS_PER_DAY)
    return ((day_index + 4) % 7) + 1
end

local function isoWeekNumber(ts)
    local day_index = math.floor(ts / SECONDS_PER_DAY)
    local dow = (day_index + 3) % 7
    return math.floor((day_index - dow + 10) / 7)
end

-- ISO week year: if week 1 belongs to next year or week belongs to prev year, adjust
local function isoWeekYear(year, month_idx, day, week)
    if week >= 52 and month_idx == 1 and day <= 3 then
        return year - 1
    elseif week == 1 and month_idx == 12 and day >= 29 then
        return year + 1
    end
    return year
end

local function tsToTime(ts)
    local secs = ts % SECONDS_PER_DAY
    if secs < 0 then secs = secs + SECONDS_PER_DAY end
    return math.floor(secs / 3600), math.floor((secs % 3600) / 60), secs % 60
end

local function buildCtx(ts)
    local date_ts = ts - DAY_START_OFFSET
    local days = math.floor(date_ts / SECONDS_PER_DAY)
    local year, month_idx, day, day_of_year = daysToDate(days)
    local hour, min, sec = tsToTime(ts)
    local lengths = getMonthLengths(year)
    local week = isoWeekNumber(date_ts)
    return {
        day         = day,
        month_idx   = month_idx,
        month_entry = BN_MONTH[month_idx],
        year        = year,
        hour        = hour,
        min         = min,
        sec         = sec,
        weekday     = tsToWeekday(date_ts),
        week        = week,
        week_year   = isoWeekYear(year, month_idx, day, week),
        day_of_year = day_of_year,
        month_days  = lengths[month_idx],
        is_leap     = isLeapYear(year),
        ts          = ts,
    }
end

local TOKEN = {
    -- year
    Y = function(c) return tostring(c.year) end,
    y = function(c) return string.format("%02d", c.year % 100) end,
    L = function(c) return c.is_leap and "1" or "0" end,
    o = function(c) return tostring(c.week_year) end,

    -- month
    n = function(c) return tostring(c.month_idx) end,
    m = function(c) return string.format("%02d", c.month_idx) end,
    M = function(c) return c.month_entry.abbr end,
    F = function(c) return c.month_entry.name end,
    t = function(c) return tostring(c.month_days) end,

    -- day
    j = function(c) return tostring(c.day) end,
    d = function(c) return string.format("%02d", c.day) end,
    z = function(c) return tostring(c.day_of_year) end,

    -- week / weekday
    W = function(c) return string.format("%02d", c.week) end,
    N = function(c)
            -- ISO: Mon=1 Sun=7; our table Sun=1 Sat=7
            return tostring(c.weekday == 1 and 7 or c.weekday - 1)
        end,
    w = function(c)
            -- Sun=0 Sat=6
            return tostring(c.weekday - 1)
        end,
    D = function(c) return BN_WEEKDAY[c.weekday].abbr end,
    l = function(c) return BN_WEEKDAY[c.weekday].name end,

    -- hour
    a = function(c) return c.hour < 12 and "am" or "pm" end,
    A = function(c) return c.hour < 12 and "AM" or "PM" end,
    g = function(c) return tostring(c.hour % 12 == 0 and 12 or c.hour % 12) end,
    h = function(c) return string.format("%02d", c.hour % 12 == 0 and 12 or c.hour % 12) end,
    G = function(c) return tostring(c.hour) end,
    H = function(c) return string.format("%02d", c.hour) end,

    -- minutes and seconds
    i = function(c) return string.format("%02d", c.min) end,
    s = function(c) return string.format("%02d", c.sec) end,

    -- unix
    U = function(c) return tostring(c.ts + BENGALI_UNIX_EPOCH) end,
    u = function(c) return tostring(c.ts) end,

    -- timezone (static, always Bangladesh)
    e = function(_) return "Asia/Dhaka" end,
    I = function(_) return "0" end,
    O = function(_) return "+0600" end,
    P = function(_) return "+06:00" end,
    T = function(_) return "BST" end,
    Z = function(_) return "21600" end,
}

local function expandToken(char, ctx)
    local fn = TOKEN[char]
    return fn and fn(ctx) or char
end

local function wrapInLink(char, expanded, month_entry)
    if (char == "F" or char == "M") and month_entry.link ~= month_entry.name then
        return "[[" .. month_entry.link .. "|" .. month_entry.name .. "]]"
    else
        return "[[" .. expanded .. "]]"
    end
end

local function formatDate(ctx, format)
    local result = {}
    local i = 1
    while i <= #format do
        local two = format:sub(i, i + 1)

        if two == "''" then
            i = i + 2
            while i <= #format do
                if format:sub(i, i + 1) == "''" then i = i + 2; break end
                table.insert(result, format:sub(i, i))
                i = i + 1
            end

        elseif format:sub(i, i) == '"' then
            i = i + 1
            while i <= #format do
                if format:sub(i, i) == '"' then i = i + 1; break end
                table.insert(result, format:sub(i, i))
                i = i + 1
            end

        elseif two == "[[" then
            i = i + 2
            local linked = {}
            while i <= #format and format:sub(i, i + 1) ~= "]]" do
                local char = format:sub(i, i)
                table.insert(linked, { char = char, expanded = expandToken(char, ctx) })
                i = i + 1
            end
            i = i + 2
            if #linked == 1 then
                table.insert(result, wrapInLink(linked[1].char, linked[1].expanded, ctx.month_entry))
            else
                local inner = {}
                for _, t in ipairs(linked) do table.insert(inner, t.expanded) end
                table.insert(result, "[[" .. table.concat(inner) .. "]]")
            end

        else
            table.insert(result, expandToken(format:sub(i, i), ctx))
            i = i + 1
        end
    end
    return table.concat(result)
end

local function isDateString(s)
    return s:match("^%d+%-%d+%-%d+$") or s:match("^%d+%s+%a+%s+%d+$")
end

local function resolveTimestamp(date_str)
    local raw, err = date_str and timestamp(date_str) or timestamp()
    if not raw then
        return nil, err or "Invalid date: " .. tostring(date_str)
    end
    return tonumber(raw), nil
end

local function parseArgs(args)
    local first = args[1] or ""
    if first == "" then
        return { date_str = nil, format = DEFAULT_FORMAT }
    elseif isDateString(first) then
        return { date_str = first, format = args[2] or DEFAULT_FORMAT }
    else
        return { date_str = args[2], format = first }
    end
end

function p.main(frame)
    local args = getArgs(frame)
    local parsed = parseArgs(args)
    local ts, err = resolveTimestamp(parsed.date_str)
    if err then
        return '<span class="error">' .. err .. '</span>'
    end
    return formatDate(buildCtx(ts), parsed.format)
end

return p