Module:Normalize volume and edition

Module used by {{Normalize volume and edition}}

Module:Normalize volume and edition

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

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.