Edge Rewrite
// request.cf · coarse context

A page that knows where it met you.

Only coarse request metadata is shown. This demo does not display or persist visitor IP addresses.

Country
US
Cloudflare location
CMH
Connection
HTTP/2
Language
Not provided

Ray ID: a4542fa4d991c20a

Jump to content

Module:Normalize volume and edition

Permanently protected module
From Wikipedia, the free encyclopedia

local p = {}

-- Master dictionary mapping written words, ordinals, and foreign terms to digits
local text_to_num = {
    ["one"] = "1", ["first"] = "1", ["1st"] = "1", ["i"] = "1",
    ["two"] = "2", ["second"] = "2", ["2nd"] = "2", ["ii"] = "2",
    ["three"] = "3", ["third"] = "3", ["3rd"] = "3", ["iii"] = "3",
    ["four"] = "4", ["fourth"] = "4", ["4th"] = "4", ["iv"] = "4",
    ["five"] = "5", ["fifth"] = "5", ["5th"] = "5", ["v"] = "5",
    ["six"] = "6", ["sixth"] = "6", ["6th"] = "6", ["vi"] = "6",
    ["seven"] = "7", ["seventh"] = "7", ["7th"] = "7", ["vii"] = "7",
    ["eight"] = "8", ["eighth"] = "8", ["8th"] = "8", ["viii"] = "8",
    ["nine"] = "9", ["ninth"] = "9", ["9th"] = "9", ["ix"] = "9",
    ["ten"] = "10", ["tenth"] = "10", ["10th"] = "10", ["x"] = "10",
    ["eleventh"] = "11", ["twelfth"] = "12", ["thirteenth"] = "13",
    ["fourteenth"] = "14", ["fifteenth"] = "15", ["sixteenth"] = "16",
    
    -- Foreign Additions (Spanish, French, German, Dutch, Italian, Latin)
    
    -- "One" / "First"
    ["uno"] = "1", ["un"] = "1", ["una"] = "1", ["ein"] = "1", ["eine"] = "1", ["eins"] = "1",
    ["een"] = "1", ["primus"] = "1", ["prima"] = "1", ["primo"] = "1", ["primer"] = "1", 
    ["primera"] = "1", ["premier"] = "1", ["première"] = "1", ["erste"] = "1", 
    ["eerste"] = "1", ["1ª"] = "1", ["初版"] = "1", ["第1巻"] = "1",

    -- "Two" / "Second"
    ["dos"] = "2", ["deux"] = "2", ["zwei"] = "2", ["twee"] = "2", 
    ["secundus"] = "2", ["secunda"] = "2", ["secundo"] = "2", ["segunda"] = "2", 
    ["deuxième"] = "2", ["zweite"] = "2", ["tweede"] = "2", ["2ª"] = "2",

    -- "Three" / "Third"
    ["tres"] = "3", ["trois"] = "3", ["drei"] = "3", ["drie"] = "3",
    ["tertius"] = "3", ["terza"] = "3", ["troisième"] = "3", ["dritte"] = "3", ["derde"] = "3",

    -- "Four" / "Fourth"
    ["cuatro"] = "4", ["quatre"] = "4", ["vier"] = "4",
    ["cuarto"] = "4", ["quatrième"] = "4", ["vierte"] = "4",

    -- "Five" / "Fifth"
    ["cinco"] = "5", ["cinq"] = "5", ["fünf"] = "5", ["vijf"] = "5",
    ["quinto"] = "5", ["cinquième"] = "5", ["fünfte"] = "5"
}

-- Simple Roman Numeral to Integer converter
local function roman_to_int(roman)
    local map = {i = 1, v = 5, x = 10, l = 50, c = 100}
    local total = 0
    local prev = 0
    
    -- iterate backwards through the string
    for i = mw.ustring.len(roman), 1, -1 do
        local char = mw.ustring.sub(roman, i, i)
        local val = map[char]
        if not val then return nil end -- invalid character found
        
        if val < prev then
            total = total - val
        else
            total = total + val
        end
        prev = val
    end
    return tostring(total)
end

local function normalize_string(input_str, type_flag)
    if not input_str or input_str == "" then return "" end
    
    local s = mw.ustring.lower(input_str)
    
    -- PREVENT THE ROMAN NUMERAL TRAP: 
    if type_flag == "volume" then
        s = mw.ustring.gsub(s, "^v%.%s*(%w+)", "%1") 
        s = mw.ustring.gsub(s, "^v%s+(%w+)", "%1")   
    end

    -- STRIP ORDINAL SUFFIXES GLOBALLY (e.g. 113th -> 113, 22nd -> 22)
    -- Using %f[%s%p] frontier pattern to ensure we only strip suffixes at the end of a word
    s = mw.ustring.gsub(s, "(%d+)st%f[%s%p]", "%1")
    s = mw.ustring.gsub(s, "(%d+)nd%f[%s%p]", "%1")
    s = mw.ustring.gsub(s, "(%d+)rd%f[%s%p]", "%1")
    s = mw.ustring.gsub(s, "(%d+)th%f[%s%p]", "%1")
    
    -- CONVERT PARENTHESIZED PARTS TO DECIMALS: "3(1)" -> "3.1"
    s = mw.ustring.gsub(s, "(%d)%s*%((%d+)%)", "%1.%2")    
    
    -- PROTECT DECIMALS
    s = mw.ustring.gsub(s, "(%d)%.(%d)", "%1ZZDOTZZ%2")
    s = mw.ustring.gsub(s, "(%d)%.(%d)", "%1ZZDOTZZ%2")

    -- PROTECT RANGES (Replace hyphens/en-dashes between numbers with a safe string)
    s = mw.ustring.gsub(s, "(%d+)%s*[-–]%s*(%d+)", "%1ZZDASHZZ%2")
    
    -- Convert all remaining punctuation to spaces
    s = mw.ustring.gsub(s, "[%p]", " ")
    
    -- RESTORE DECIMALS AND RANGES (Standardizing ranges to en-dash)
    s = mw.ustring.gsub(s, "ZZDOTZZ", ".")
    s = mw.ustring.gsub(s, "ZZDASHZZ", "–")
    
    -- SQUISH HANGING LETTERS
    s = " " .. s .. " "
    s = mw.ustring.gsub(s, "(%d)%s+([a-z])%s+", "%1%2 ")
    s = mw.ustring.match(s, "^%s*(.-)%s*$")
    
    -- Break the string into individual words/tokens
    for word in mw.ustring.gmatch(s, "%S+") do
        
        -- 1. Check Dictionary (Catches words, foreign ordinals, single letters 'i', 'v')
        if text_to_num[word] then
            return text_to_num[word]
        end

        -- 2. Check Roman Numerals (Only allows standard roman characters)
        if mw.ustring.match(word, "^[ivxlc]+$") then
            local arabic = roman_to_int(word)
            if arabic then return arabic end
        end
        
        -- 3. Check for standard Numbers, Decimals, or "Number+Letter" (e.g., 3, 3.1, 3a)
        if mw.ustring.match(word, "^%d+%.?%d*[a-z]?$") then
            return word
        end

        -- 3.5 Check for protected Ranges (e.g., 1–3)
        if mw.ustring.match(word, "^%d+–%d+$") then
            return word
        end
        
        -- 4. Check for letter-first alphanumeric volumes (e.g., "A1")
        if mw.ustring.match(word, "^[a-z]%d+$") then
            return mw.ustring.upper(word)
        end
        
        -- 5. Volume single-letter fallback (e.g., "A", "b")
        if type_flag == "volume" then
            if mw.ustring.match(word, "^[a-z]$") and word ~= "v" then 
                return mw.ustring.upper(word)
            end
        end
    end

    -- Return the original input if nothing recognized is found
    return mw.text.trim(input_str)
end

function p.volume(frame)
    local args = frame.args[1] and frame.args or frame:getParent().args
    return normalize_string(args[1], "volume")
end

function p.edition(frame)
    local args = frame.args[1] and frame.args or frame:getParent().args
    return normalize_string(args[1], "edition")
end

return p