Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

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