Jump to content

Module:CountryData: Difference between revisions

// via Wikitext Extension for VSCode
Tag: Reverted
// via Wikitext Extension for VSCode
 
(43 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:CountryData
--[[
-- Unified module for country data management.
* Name: CountryData
--
* Author: Mark W. Datysgeld
-- Features:
* Description: Unified module for country data management with JSON loading, normalization, region mapping, and Semantic MediaWiki integration
--  * Loads country data from JSON stored in Data:CountryDataset.json
* Notes: Loads from Data:CountryDataset.json; normalizes country names to canonical forms; maps countries to ICANN regions; provides extensible property access; formats country lists with region-specific emoji styling; processes countries for category assignment
--  * Normalizes country names to canonical forms
]]
--  * Maps countries to ICANN regions
--  * Provides extensible property access
--  * Integrates with Semantic MediaWiki
--  * Formats country lists with region-specific emoji styling
--  * Processes countries for category assignment


-- Dependencies
-- Dependencies
local DiacriticNormalization = require('Module:NormalizationDiacritic')
local DiacriticNormalization = require('Module:NormalizationDiacritic')
local NormalizationText = require('Module:NormalizationText')
local NormalizationText = require('Module:NormalizationText')
local loader = require('Module:DatasetLoader')


-- Module-level cache tables for improved performance
-- Module-level cache tables for improved performance
Line 44: Line 40:
end
end


-- Function to safely check if a table has a property
-- Reset the module-level caches (useful for testing)
local function hasProperty(tbl, property)
local function resetCaches()
     return tbl and type(tbl) == "table" and tbl[property] ~= nil
     dataCache = nil
    nameLookupCache = nil
    regionLookupCache = nil
    propertyCache = {}
    functionCache = {}
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Data Loading Layer
-- Data Loading and Cache Building Layer (Refactored)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Get name lookup cache: builds if not already cached
-- Data loading function using DatasetLoader
local function getNameLookup(data)
local function loadData()
    if dataCache then
        return dataCache
    end
    local raw = loader.get('CountryDataset')
    dataCache = {
        countries      = raw.countries      or {},
        icann_regions  = raw.icann_regions  or {},
        schema_version = raw.schema_version,
        last_updated  = raw.last_updated
    }
    return dataCache
end
 
-- Builds the primary name-to-code lookup cache.
-- This is the core of the refactoring, ensuring correct normalization order.
local function buildNameLookupCache(data)
     if nameLookupCache then
     if nameLookupCache then
         return nameLookupCache
         return nameLookupCache
     end
     end
   
 
    -- If no data provided, return empty lookup
     if not data or not data.countries then
     if not data or not data.countries then
         nameLookupCache = {}
         nameLookupCache = {}
         return nameLookupCache
         return nameLookupCache
     end
     end
   
 
     local lookup = {}
     local lookup = {}
   
    -- Optimization: Pre-count number of mappings
    local mappingCount = 0
     for code, country in pairs(data.countries) do
     for code, country in pairs(data.countries) do
         -- Count canonical name
         -- Ensure the country has a name to process
         mappingCount = mappingCount + 1
         local canonicalName = country.name or country.canonical_name
          
         if canonicalName then
        -- Count variations if they exist
            -- 1. Add the canonical name itself
        if country.variations and type(country.variations) == "table" then
            local normalizedName = NormalizationText.normalizeText(canonicalName)
             mappingCount = mappingCount + #country.variations
            lookup[normalizedName] = code
           
            -- 2. Add the diacritic-stripped version of the canonical name
            local strippedName = DiacriticNormalization.removeDiacritics(canonicalName)
             if strippedName ~= canonicalName then -- only add if it's different
                lookup[NormalizationText.normalizeText(strippedName)] = code
            end
         end
         end
       
 
         -- Count ISO-3 code if it exists
         -- 3. Process all variations
        if country.iso_3 then
            mappingCount = mappingCount + 1
        end
    end
   
    -- Build the lookup table with pre-counted size
    for code, country in pairs(data.countries) do
        -- Add name field as primary display name
        local displayName = country.name or country.canonical_name
        if displayName then
            lookup[NormalizationText.normalizeText(displayName)] = code
        end
       
        -- Add canonical_name if different from name
        if country.canonical_name and country.canonical_name ~= country.name then
            lookup[NormalizationText.normalizeText(country.canonical_name)] = code
        end
       
        -- Add variations
         if country.variations and type(country.variations) == "table" then
         if country.variations and type(country.variations) == "table" then
             for _, variation in ipairs(country.variations) do
             for _, variation in pairs(country.variations) do
                 lookup[NormalizationText.normalizeText(variation)] = code
                -- Add the variation
                local normalizedVariation = NormalizationText.normalizeText(variation)
                 lookup[normalizedVariation] = code
               
                -- Add the diacritic-stripped version of the variation
                local strippedVariation = DiacriticNormalization.removeDiacritics(variation)
                if strippedVariation ~= variation then
                    lookup[NormalizationText.normalizeText(strippedVariation)] = code
                end
             end
             end
        end
       
        -- Add ISO-3 code as a variation (both original and normalized)
        if country.iso_3 then
            lookup[country.iso_3] = code
            lookup[NormalizationText.normalizeText(country.iso_3)] = code
         end
         end
     end
     end
   
 
     nameLookupCache = lookup
     nameLookupCache = lookup
     return lookup
     return lookup
end
end


-- Get region lookup cache: builds if not already cached
-- Builds the region lookup cache.
local function getRegionLookup(data)
local function buildRegionLookupCache(data)
     if regionLookupCache then
     if regionLookupCache then
         return regionLookupCache
         return regionLookupCache
     end
     end
   
 
    -- If no data provided, return empty lookup
     if not data or not data.icann_regions then
     if not data or not data.icann_regions then
         regionLookupCache = {}
         regionLookupCache = {}
         return regionLookupCache
         return regionLookupCache
     end
     end
   
 
     local lookup = {}
     local lookup = {}
   
    -- Optimization: Pre-count number of mappings
    local mappingCount = 0
    for code, region in pairs(data.icann_regions) do
        -- Count canonical name
        mappingCount = mappingCount + 1
       
        -- Count variations if they exist
        if region.variations and type(region.variations) == "table" then
            mappingCount = mappingCount + #region.variations
        end
    end
   
    -- Build the lookup table with pre-counted size
     for code, region in pairs(data.icann_regions) do
     for code, region in pairs(data.icann_regions) do
        -- Add canonical name
         if region.name then
         if region.name then
             lookup[NormalizationText.normalizeText(region.name)] = code
             lookup[NormalizationText.normalizeText(region.name)] = code
         end
         end
       
        -- Add variations
         if region.variations and type(region.variations) == "table" then
         if region.variations and type(region.variations) == "table" then
             for _, variation in ipairs(region.variations) do
             for _, variation in pairs(region.variations) do
                 lookup[NormalizationText.normalizeText(variation)] = code
                 lookup[NormalizationText.normalizeText(variation)] = code
             end
             end
         end
         end
     end
     end
   
 
     regionLookupCache = lookup
     regionLookupCache = lookup
     return lookup
     return lookup
end
-- Main data loading function with multiple fallback methods
local function loadData(frame)
    if dataCache then
        return dataCache
    end
    local loader = require('Module:DatasetLoader')
    local data = loader.get('CountryDataset')
    -- Fallback to raw JSON via frame:preprocess
    local jsonText
    if (not data or type(data) ~= 'table' or not data.countries or next(data.countries) == nil)
      and frame and type(frame) == 'table' and frame.preprocess then
        local ok, res = pcall(frame.preprocess, frame, '{{Data:CountryDataset.json}}')
        if ok and res then
            jsonText = res
        end
    end
    -- Fallback to mw.loadJsonData if available and no jsonText
    if not jsonText and mw.loadJsonData then
        local ok, tbl = pcall(mw.loadJsonData, 'Data:CountryDataset.json')
        if ok and type(tbl) == 'table' then
            data = tbl
        end
    end
    -- Decode jsonText if present
    if jsonText and mw.text and mw.text.jsonDecode then
        local ok, tbl = pcall(mw.text.jsonDecode, jsonText)
        if ok and type(tbl) == 'table' then
            data = tbl
        end
    end
    -- Fallback to DEFAULT_DATA if still no countries
    if not data or type(data) ~= 'table' or not data.countries then
        data = DEFAULT_DATA
    end
    -- Ensure minimum data structure
    data.countries = data.countries or {}
    data.icann_regions = data.icann_regions or {}
    dataCache = data
    return dataCache
end
-- Reset the module-level caches (useful for testing)
local function resetCaches()
    dataCache = nil
    nameLookupCache = nil
    regionLookupCache = nil
    propertyCache = {}
    functionCache = {}
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Core API Functions
-- Core API Functions (Public Interface)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


Line 225: Line 151:
-- Load data and initialize caches
-- Load data and initialize caches
function CountryData.loadData(frame)
function CountryData.loadData(frame)
     return loadData(frame)
     return loadData()
end
end


Line 234: Line 160:
end
end


-- Get country data by ISO code
-- Get country data by ISO code (Refactored to use new cache logic)
function CountryData.getCountryByCode(code)
function CountryData.getCountryByCode(code)
     if not code or code == "" then
     if not code or code == "" then
Line 240: Line 166:
     end
     end
      
      
    -- Check function cache first
     local cacheKey = createCacheKey("getCountryByCode", code)
     local cacheKey = createCacheKey("getCountryByCode", code)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 247: Line 172:
      
      
     local data = loadData()
     local data = loadData()
    code = code:upper() -- Standardize code
      
      
     -- Standardize code to uppercase for consistency
     local result = data and data.countries and data.countries[code] or nil
    code = code:upper()
      
      
    local result = nil
    if data and data.countries and data.countries[code] then
        result = data.countries[code]
    end
   
    -- Cache the result (including nil)
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
end
end


-- Get country data by name (including variations)
-- Get country data by name (Refactored to use new cache logic)
function CountryData.getCountryByName(name)
function CountryData.getCountryByName(name)
     if not name or name == "" then
     if not name or name == "" then
         return nil
         return nil
     end
     end
   
 
    -- Check function cache first
     local cacheKey = createCacheKey("getCountryByName", name)
     local cacheKey = createCacheKey("getCountryByName", name)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
         return functionCache[cacheKey]
         return functionCache[cacheKey]
     end
     end
   
 
     local data = loadData()
     local data = loadData()
     local nameLookup = getNameLookup(data)
     local nameLookup = buildNameLookupCache(data)
   
 
     -- Normalize the input
     -- Normalize the input name in one go (text normalization includes lowercasing)
     local normalized = NormalizationText.normalizeText(name)
     local normalized = NormalizationText.normalizeText(name)
      
      
     -- Look up the code
     -- First, try a direct lookup with the normalized name
     local code = nameLookup[normalized]
     local code = nameLookup[normalized]
      
 
     -- If not found, try looking up the diacritic-stripped version
    if not code then
        local stripped = DiacriticNormalization.removeDiacritics(name)
        if stripped ~= name then
            code = nameLookup[NormalizationText.normalizeText(stripped)]
        end
    end
 
     local result = nil
     local result = nil
     if code and data.countries[code] then
     if code then
         result = data.countries[code]
         result = data.countries[code]
    else
        -- Try with diacritics removed
        local stripped = DiacriticNormalization.removeDiacritics(normalized)
        if stripped ~= normalized then
            code = nameLookup[stripped]
            if code and data.countries[code] then
                result = data.countries[code]
            end
        end
     end
     end
   
 
    -- Cache the result (including nil)
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
Line 307: Line 223:
     end
     end
      
      
    -- Check function cache first
     local cacheKey = createCacheKey("getCountryCodeByName", name)
     local cacheKey = createCacheKey("getCountryCodeByName", name)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 313: Line 228:
     end
     end
      
      
     local data = loadData()
     local country = CountryData.getCountryByName(name)
    local nameLookup = getNameLookup(data)
      
      
     -- Normalize the input
     -- The country object from the dataset doesn't inherently contain its own ISO code key.
    local normalized = NormalizationText.normalizeText(name)
     -- We must iterate through the dataset to find the key corresponding to the found country object.
   
     if country then
     -- Look up the code
         local data = loadData()
    local code = nameLookup[normalized]
         for code, countryData in pairs(data.countries) do
   
            if countryData == country then
     if not code then
                functionCache[cacheKey] = code
        -- Try with diacritics removed
                return code
         local stripped = DiacriticNormalization.removeDiacritics(normalized)
            end
         if stripped ~= normalized then
            code = nameLookup[stripped]
         end
         end
     end
     end
   
 
     -- Cache the result (including nil)
     -- If no country was found, or no matching code was found, cache and return nil.
     functionCache[cacheKey] = code
     functionCache[cacheKey] = nil
     return code
     return nil
end
end
-- (The rest of the functions remain unchanged for now, but will be updated in subsequent phases)


function CountryData.normalizeCountryName(name)
function CountryData.normalizeCountryName(name)
     if not name or name == "" then
     if not name or name == "" then
         return name
         return "(Unrecognized)"
     end
     end
      
      
    -- Check function cache first
     local cacheKey = createCacheKey("normalizeCountryName", name)
     local cacheKey = createCacheKey("normalizeCountryName", name)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 349: Line 262:
      
      
     local result
     local result
     if country then
     if country and country.name then
        -- Return name as the primary display name
         result = country.name
         result = country.name or country.canonical_name
        -- make it category‑friendly:
        result = result
            :gsub(",%s*", "")                -- drop any commas
            :gsub("%sand the%s+", " and ")    -- turn “ and the ” into “ and ”
     else
     else
        -- If no match, return "(Unrecognized)"
         result = "(Unrecognized)"
         result = "(Unrecognized)"
     end
     end
      
      
    -- Cache the result
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
end
end


-- Get ICANN region for a country
function CountryData.getRegionByCountry(name)
function CountryData.getRegionByCountry(name)
     if not name or name == "" then
     if not name or name == "" then
         return nil
         return "(Unrecognized)"
     end
     end
      
      
    -- Check function cache first
     local cacheKey = createCacheKey("getRegionByCountry", name)
     local cacheKey = createCacheKey("getRegionByCountry", name)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 384: Line 288:
         result = country.icann_region
         result = country.icann_region
     else
     else
        -- Return "(Unrecognized)" for consistency with normalizeCountryName
         result = "(Unrecognized)"
         result = "(Unrecognized)"
     end
     end
      
      
    -- Cache the result
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
end
end


-- Get all countries in a specific region
function CountryData.getCountriesByRegion(region)
function CountryData.getCountriesByRegion(region)
     if not region or region == "" then
     if not region or region == "" then
Line 399: Line 300:
     end
     end
      
      
    -- Check function cache first
     local cacheKey = createCacheKey("getCountriesByRegion", region)
     local cacheKey = createCacheKey("getCountriesByRegion", region)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 406: Line 306:
      
      
     local data = loadData()
     local data = loadData()
     local regionLookup = getRegionLookup(data)
     local regionLookup = buildRegionLookupCache(data)
      
      
    -- Normalize the input
     local normalized = NormalizationText.normalizeText(region)
     local normalized = NormalizationText.normalizeText(region)
   
    -- Look up the region code
     local regionCode = regionLookup[normalized]
     local regionCode = regionLookup[normalized]
      
      
     local result = {}
     local result = {}
     if regionCode and data.countries then
     if regionCode and data.countries then
        -- Pre-count number of countries in region for allocation
        local countryCount = 0
        for _, country in pairs(data.countries) do
            if country.icann_region == regionCode then
                countryCount = countryCount + 1
            end
        end
       
        -- Populate the result with the pre-allocated size
        local index = 1
         for code, country in pairs(data.countries) do
         for code, country in pairs(data.countries) do
             if country.icann_region == regionCode then
             if country.icann_region == regionCode then
                 result[index] = {
                 table.insert(result, {
                     code = code,
                     code = code,
                     name = country.name or country.canonical_name
                     name = country.name
                 }
                 })
                index = index + 1
             end
             end
         end
         end
     end
     end
      
      
    -- Cache the result
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
end
end


-- Get list of all country codes
function CountryData.getAllCountryCodes()
function CountryData.getAllCountryCodes()
    -- Check function cache first
     local cacheKey = "getAllCountryCodes"
     local cacheKey = "getAllCountryCodes"
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 452: Line 335:
     local data = loadData()
     local data = loadData()
     local result = {}
     local result = {}
   
     if data and data.countries then
     if data and data.countries then
        -- Pre-allocate the array to the number of countries
        local countryCount = 0
        for _ in pairs(data.countries) do
            countryCount = countryCount + 1
        end
       
        -- Now populate the array
        local index = 1
         for code in pairs(data.countries) do
         for code in pairs(data.countries) do
             result[index] = code
             table.insert(result, code)
            index = index + 1
         end
         end
     end
     end
      
      
    -- Cache the result
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
end
end


-- Get list of all canonical country names
function CountryData.getAllCountryNames()
function CountryData.getAllCountryNames()
    -- Check function cache first
     local cacheKey = "getAllCountryNames"
     local cacheKey = "getAllCountryNames"
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 483: Line 353:
     local data = loadData()
     local data = loadData()
     local result = {}
     local result = {}
   
     if data and data.countries then
     if data and data.countries then
        -- Pre-allocate the array to the number of countries
        local countryCount = 0
        for _ in pairs(data.countries) do
            countryCount = countryCount + 1
        end
       
        -- Populate the array
        local index = 1
         for _, country in pairs(data.countries) do
         for _, country in pairs(data.countries) do
             local name = country.name or country.canonical_name
             table.insert(result, country.name)
            result[index] = name
            index = index + 1
         end
         end
     end
     end
      
      
    -- Cache the result
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
end
end


-- Get any property for a country by code
function CountryData.getCountryProperty(code, property)
function CountryData.getCountryProperty(code, property)
     if not code or code == "" or not property or property == "" then
     if not code or code == "" or not property or property == "" then
Line 511: Line 368:
     end
     end
      
      
    -- Check property cache first
     local cacheKey = createCacheKey("getCountryProperty", code, property)
     local cacheKey = createCacheKey("getCountryProperty", code, property)
     if propertyCache[cacheKey] ~= nil then
     if propertyCache[cacheKey] ~= nil then
Line 518: Line 374:
      
      
     local country = CountryData.getCountryByCode(code)
     local country = CountryData.getCountryByCode(code)
    local result = country and country[property] or nil
      
      
    local result = nil
    if country and country[property] ~= nil then
        result = country[property]
    end
   
    -- Cache the result (including nil)
     propertyCache[cacheKey] = result
     propertyCache[cacheKey] = result
     return result
     return result
end
end


-- Get any property for a country by name
function CountryData.getCountryPropertyByName(name, property)
function CountryData.getCountryPropertyByName(name, property)
     if not name or name == "" or not property or property == "" then
     if not name or name == "" or not property or property == "" then
Line 535: Line 385:
     end
     end
      
      
    -- Check property cache first
     local cacheKey = createCacheKey("getCountryPropertyByName", name, property)
     local cacheKey = createCacheKey("getCountryPropertyByName", name, property)
     if propertyCache[cacheKey] ~= nil then
     if propertyCache[cacheKey] ~= nil then
Line 542: Line 391:
      
      
     local code = CountryData.getCountryCodeByName(name)
     local code = CountryData.getCountryCodeByName(name)
   
     local result = nil
     local result = nil
     if code then
     if code then
Line 548: Line 396:
     end
     end
      
      
    -- Cache the result (including nil)
     propertyCache[cacheKey] = result
     propertyCache[cacheKey] = result
     return result
     return result
end
end


-- List all available properties for a country
function CountryData.getAvailableProperties(code)
function CountryData.getAvailableProperties(code)
     if not code or code == "" then
     if not code or code == "" then
Line 559: Line 405:
     end
     end
      
      
    -- Check function cache first
     local cacheKey = createCacheKey("getAvailableProperties", code)
     local cacheKey = createCacheKey("getAvailableProperties", code)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 565: Line 410:
     end
     end
      
      
     local data = loadData()
     local country = CountryData.getCountryByCode(code)
    if not data or not data.countries or not data.countries[code] then
        return {}
    end
   
     local properties = {}
     local properties = {}
      
     if country then
    -- Pre-allocate the table based on the number of properties
        for property in pairs(country) do
    local propertyCount = 0
            table.insert(properties, property)
    for _ in pairs(data.countries[code]) do
         end
        propertyCount = propertyCount + 1
    end
   
    -- Fill the table with property names
    local index = 1
    for property in pairs(data.countries[code]) do
        properties[index] = property
         index = index + 1
     end
     end
      
      
    -- Cache the result
     functionCache[cacheKey] = properties
     functionCache[cacheKey] = properties
     return properties
     return properties
end
end


-- Get all unique property names across all countries
function CountryData.getAllPropertyNames()
function CountryData.getAllPropertyNames()
    -- Check function cache first
     local cacheKey = "getAllPropertyNames"
     local cacheKey = "getAllPropertyNames"
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 599: Line 429:
      
      
     local data = loadData()
     local data = loadData()
    if not data or not data.countries then
        return {}
    end
   
     local properties = {}
     local properties = {}
     local seen = {}
     local seen = {}
      
     if data and data.countries then
    -- First pass: count unique properties for pre-allocation
        for _, country in pairs(data.countries) do
    local propertyCount = 0
            for property in pairs(country) do
    for _, country in pairs(data.countries) do
                if not seen[property] then
        for property in pairs(country) do
                    seen[property] = true
            if not seen[property] then
                    table.insert(properties, property)
                seen[property] = true
                 end
                 propertyCount = propertyCount + 1
             end
             end
         end
         end
     end
     end
      
      
    -- Reset seen table
    seen = {}
   
    -- Second pass: fill the pre-allocated table
    local index = 1
    for _, country in pairs(data.countries) do
        for property in pairs(country) do
            if not seen[property] then
                seen[property] = true
                properties[index] = property
                index = index + 1
            end
        end
    end
   
    -- Cache the result
     functionCache[cacheKey] = properties
     functionCache[cacheKey] = properties
     return properties
     return properties
end
end


-- Get semantic property name from ConfigRepository
function CountryData.getSemanticPropertyName(propertyKey)
    local ConfigRepository = require('Module:ConfigRepository')
   
    -- Look through all template configurations
    for templateName, templateConfig in pairs(ConfigRepository.templates) do
        -- Check if this template has semantics configuration
        if templateConfig.semantics and templateConfig.semantics.additionalProperties then
            -- Check if the property key exists in additionalProperties
            if templateConfig.semantics.additionalProperties[propertyKey] then
                return propertyKey
            end
        end
    end
   
    -- If not found, return nil
    return nil
end
-- Get semantic properties for countries and regions
-- Returns a table of properties that can be integrated with the batch processing system
function CountryData.getSemanticCountryRegionProperties(countryValue)
function CountryData.getSemanticCountryRegionProperties(countryValue)
    -- Initialize return table
     local properties = {}
     local properties = {}
   
     if not countryValue or countryValue == "" then
     if not countryValue or countryValue == "" then
         return properties
         return properties
     end
     end
      
      
     -- Get property names from ConfigRepository
     local ConfigRepository = require('Module:ConfigRepository')
     local countryPropertyName = CountryData.getSemanticPropertyName("Has country")
     local countryPropertyName = ConfigRepository.getSemanticPropertyName("Has country")
     local regionPropertyName = CountryData.getSemanticPropertyName("Has ICANN region")
     local regionPropertyName = ConfigRepository.getSemanticPropertyName("Has ICANN region")
      
      
    -- If property names are not found in ConfigRepository, we can't proceed
     if not countryPropertyName or not regionPropertyName then
     if not countryPropertyName or not regionPropertyName then
         return properties
         return properties
     end
     end
      
      
    -- Split multi-value country strings
     local countries = {}
     local countries = {}
     for country in string.gmatch(countryValue, "[^;]+") do
     for country in string.gmatch(countryValue, "[^;]+") do
Line 685: Line 468:
     end
     end
      
      
    -- Process each country
     for _, countryName in ipairs(countries) do
     for _, country in ipairs(countries) do
         local normalizedCountry = CountryData.normalizeCountryName(countryName)
         local normalizedCountry = CountryData.normalizeCountryName(country)
       
        -- Only process recognized countries
         if normalizedCountry ~= "(Unrecognized)" then
         if normalizedCountry ~= "(Unrecognized)" then
             -- Add country to properties table
             -- Initialize property tables if they don't exist
             properties[countryPropertyName] = properties[countryPropertyName] or {}
             properties[countryPropertyName] = properties[countryPropertyName] or {}
             table.insert(properties[countryPropertyName], normalizedCountry)
             table.insert(properties[countryPropertyName], normalizedCountry)
              
              
            -- Add region to properties table
             local region = CountryData.getRegionByCountry(normalizedCountry)
             local region = CountryData.getRegionByCountry(country)
             if region and region ~= "(Unrecognized)" then
             if region and region ~= "(Unrecognized)" then
                 properties[regionPropertyName] = properties[regionPropertyName] or {}
                 properties[regionPropertyName] = properties[regionPropertyName] or {}
Line 707: Line 486:
end
end


-- Export country data as JSON string (for JavaScript usage)
function CountryData.exportAsJson()
function CountryData.exportAsJson()
     local data = loadData()
     local data = loadData()
   
    -- Ensure we have valid data
     if not data or not data.countries then
     if not data or not data.countries then
         return '{}'
         return '{}'
     end
     end
      
      
    -- Use MediaWiki's JSON encoder
     if mw.text and mw.text.jsonEncode then
     if mw.text and mw.text.jsonEncode then
         local success, result = pcall(function()
         local success, result = pcall(function()
             return mw.text.jsonEncode(data)
             return mw.text.jsonEncode(data)
         end)
         end)
       
         if success and result then
         if success and result then
             return result
             return result
         end
         end
     end
     end
   
    -- Fallback to simple string if JSON encoding fails
     return '{}'
     return '{}'
end
end


--------------------------------------------------------------------------------
-- Country Display Functions with contextual emoji
--------------------------------------------------------------------------------
-- Get region-specific CSS class for country display
local function getRegionClass(region)
local function getRegionClass(region)
     if not region or region == "(Unrecognized)" then
     if not region or region == "(Unrecognized)" then
         return "region-default"
         return "region-default"
     end
     end
   
     if region == "NA" or region == "LAC" then
     if region == "NA" or region == "LAC" then
         return "region-americas"
         return "region-americas"
Line 749: Line 516:
end
end


-- Format a list of countries from a semicolon-separated string
-- Returns either plain text (single country) or bullet points (multiple countries)
-- Each country gets its own region-specific class for styling
function CountryData.formatCountryList(value)
function CountryData.formatCountryList(value)
     if not value or value == "" then return "" end
     if not value or value == "" then return "" end
      
 
     -- Split and normalize countries
     local ListGeneration = require('Module:ListGeneration')
     local countries = {}
    local itemsToProcess = {}
     for country in string.gmatch(value, "[^;]+") do
 
        local trimmed = country:match("^%s*(.-)%s*$")
    -- First, check if the entire string is a single, valid country.
        if trimmed and trimmed ~= "" then
     -- This correctly handles names like "Trinidad and Tobago".
            table.insert(countries, trimmed)
     local singleCountryName = CountryData.normalizeCountryName(value)
    if singleCountryName ~= "(Unrecognized)" then
        -- If it's a valid country, treat it as a single item.
        table.insert(itemsToProcess, value)
     else
        -- If not a single country, assume it's a list and split ONLY by semicolon.
        -- This is safer than letting ListGeneration guess the delimiter.
        for item in string.gmatch(value, "[^;]+") do
            local trimmed = item:match("^%s*(.-)%s*$")
            if trimmed and trimmed ~= "" then
                table.insert(itemsToProcess, trimmed)
            end
         end
         end
     end
     end
   
 
     local normalizedCountries = {}
     -- Define the item hook for country-specific formatting
     local validCountriesCount = 0
     local function countryItemHook(countryName)
   
         local normalized = CountryData.normalizeCountryName(countryName)
    for _, country in ipairs(countries) do
         local normalized = CountryData.normalizeCountryName(country)
        -- Only include recognized countries
         if normalized ~= "(Unrecognized)" then
         if normalized ~= "(Unrecognized)" then
             validCountriesCount = validCountriesCount + 1
             local countryRegion = CountryData.getRegionByCountry(normalized)
             normalizedCountries[validCountriesCount] = normalized
            -- Return a table with content and class for the li element
             return {
                content = normalized,
                class = getRegionClass(countryRegion)
            }
         end
         end
        return nil -- Exclude unrecognized countries from the list
     end
     end
   
 
     -- Generate output based on number of countries
     -- Set the options for the list generation
     if validCountriesCount > 1 then
     local options = {
        local listItems = {}
         mode = 'bullet',
       
         listClass = 'template-list-country',
         for _, country in ipairs(normalizedCountries) do
         itemHook = countryItemHook
            -- Get the region for this specific country
    }
            local countryRegion = CountryData.getRegionByCountry(country)
 
            local regionClass = getRegionClass(countryRegion)
    -- Pass the pre-processed table of items to the list generator.
           
    return ListGeneration.createList(itemsToProcess, options)
            -- Create a list item with region-specific class
            table.insert(listItems, string.format("<li class=\"%s\">%s</li>", regionClass, country))
        end
          
        return string.format("<ul class=\"template-list template-list-country\">%s</ul>",  
                            table.concat(listItems, ""))
    elseif validCountriesCount == 1 then
        -- For a single country, create a similar list with just one item
         local countryRegion = CountryData.getRegionByCountry(normalizedCountries[1])
        local regionClass = getRegionClass(countryRegion)
       
        -- Single item list with the same styling
        return string.format("<ul class=\"template-list template-list-country\"><li class=\"%s\">%s</li></ul>",
                            regionClass, normalizedCountries[1])
    end
   
    return ""
end
end


-- Alias for backward compatibility
function CountryData.formatCountries(value)
function CountryData.formatCountries(value)
     return CountryData.formatCountryList(value)
     return CountryData.formatCountryList(value)
end
end


-- Get a list of normalized countries for category assignment
function CountryData.getCountriesForCategories(value)
function CountryData.getCountriesForCategories(value)
     if not value or value == "" then return {} end
     if not value or value == "" then return {} end
      
      
     local countries = {}
     local countries = {}
     for country in string.gmatch(value, "[^;]+") do
     for countryName in string.gmatch(value, "[^;]+") do
         local trimmed = country:match("^%s*(.-)%s*$")
         local trimmed = countryName:match("^%s*(.-)%s*$")
         if trimmed and trimmed ~= "" then
         if trimmed and trimmed ~= "" then
             table.insert(countries, trimmed)
             local normalized = CountryData.normalizeCountryName(trimmed)
            if normalized ~= "(Unrecognized)" then
                table.insert(countries, normalized)
            end
         end
         end
     end
     end
      
      
     local normalizedCountries = {}
    return countries
     local validCount = 0
end
 
function CountryData.getFlagFileName(countryNameOrCode)
    if not countryNameOrCode or countryNameOrCode == '' then return nil end
   
     local inputName = countryNameOrCode:gsub('_', ' ')
     local isoCode
   
    -- First, try to get the ISO code by treating inputName as a country name.
    isoCode = CountryData.getCountryCodeByName(inputName)
      
      
     for _, country in ipairs(countries) do
     -- If no code was found by name, and the inputName itself is 2 characters long,
        local normalized = CountryData.normalizeCountryName(country)
    -- it might be an ISO code already. Let's validate it.
        -- Only include recognized countries
    if not isoCode and #inputName == 2 then
         if normalized ~= "(Unrecognized)" then
         if CountryData.getCountryByCode(inputName) then  
             validCount = validCount + 1
             isoCode = inputName:upper()
            normalizedCountries[validCount] = normalized
         end
         end
     end
     end
      
      
     return normalizedCountries
     if not isoCode or #isoCode ~= 2 then return nil end
end


-- Return the module for use
     return 'Flag-' .. string.lower(isoCode) .. '.svg'  
-- Adds flag filename lookup
function CountryData.getFlagFileName(countryNameOrCode)
     if not countryNameOrCode or countryNameOrCode == '' then return nil end
    -- Normalize input: replace underscores with spaces
    local input = countryNameOrCode:gsub('_', ' ')
    local code
    if #input == 2 then
        code = string.upper(input)
    else
        -- Normalize country name to canonical form
        local norm = CountryData.normalizeCountryName(input)
        code = CountryData.getCountryCodeByName(norm)
    end
    if not code or code == '' then return nil end
    return 'Flag_of_' .. code .. '.svg'
end
end


return CountryData
return CountryData

Latest revision as of 02:57, 25 August 2025

Documentation for this module may be created at Module:CountryData/doc

--[[
* Name: CountryData
* Author: Mark W. Datysgeld
* Description: Unified module for country data management with JSON loading, normalization, region mapping, and Semantic MediaWiki integration
* Notes: Loads from Data:CountryDataset.json; normalizes country names to canonical forms; maps countries to ICANN regions; provides extensible property access; formats country lists with region-specific emoji styling; processes countries for category assignment
]]

-- Dependencies
local DiacriticNormalization = require('Module:NormalizationDiacritic')
local NormalizationText = require('Module:NormalizationText')
local loader = require('Module:DatasetLoader')

-- Module-level cache tables for improved performance
local dataCache = nil
local nameLookupCache = nil
local regionLookupCache = nil
local propertyCache = {}
local functionCache = {}

-- Default data structure to use if JSON loading fails
local DEFAULT_DATA = {
    schema_version = 1,
    last_updated = os.date('!%Y-%m-%dT%H:%M:%SZ'),
    countries = {},
    icann_regions = {}
}

--------------------------------------------------------------------------------
-- Helper Functions
--------------------------------------------------------------------------------

-- Create a cache key from a function name and arguments
local function createCacheKey(funcName, ...)
    local args = {...}
    local keyParts = {funcName}
    for i = 1, #args do
        table.insert(keyParts, tostring(args[i]) or "nil")
    end
    return table.concat(keyParts, ":")
end

-- Reset the module-level caches (useful for testing)
local function resetCaches()
    dataCache = nil
    nameLookupCache = nil
    regionLookupCache = nil
    propertyCache = {}
    functionCache = {}
end

--------------------------------------------------------------------------------
-- Data Loading and Cache Building Layer (Refactored)
--------------------------------------------------------------------------------

-- Data loading function using DatasetLoader
local function loadData()
    if dataCache then
        return dataCache
    end
    local raw = loader.get('CountryDataset')
    dataCache = {
        countries      = raw.countries      or {},
        icann_regions  = raw.icann_regions  or {},
        schema_version = raw.schema_version,
        last_updated   = raw.last_updated
    }
    return dataCache
end

-- Builds the primary name-to-code lookup cache.
-- This is the core of the refactoring, ensuring correct normalization order.
local function buildNameLookupCache(data)
    if nameLookupCache then
        return nameLookupCache
    end

    if not data or not data.countries then
        nameLookupCache = {}
        return nameLookupCache
    end

    local lookup = {}
    for code, country in pairs(data.countries) do
        -- Ensure the country has a name to process
        local canonicalName = country.name or country.canonical_name
        if canonicalName then
            -- 1. Add the canonical name itself
            local normalizedName = NormalizationText.normalizeText(canonicalName)
            lookup[normalizedName] = code
            
            -- 2. Add the diacritic-stripped version of the canonical name
            local strippedName = DiacriticNormalization.removeDiacritics(canonicalName)
            if strippedName ~= canonicalName then -- only add if it's different
                 lookup[NormalizationText.normalizeText(strippedName)] = code
            end
        end

        -- 3. Process all variations
        if country.variations and type(country.variations) == "table" then
            for _, variation in pairs(country.variations) do
                -- Add the variation
                local normalizedVariation = NormalizationText.normalizeText(variation)
                lookup[normalizedVariation] = code
                
                -- Add the diacritic-stripped version of the variation
                local strippedVariation = DiacriticNormalization.removeDiacritics(variation)
                if strippedVariation ~= variation then
                    lookup[NormalizationText.normalizeText(strippedVariation)] = code
                end
            end
        end
    end

    nameLookupCache = lookup
    return lookup
end

-- Builds the region lookup cache.
local function buildRegionLookupCache(data)
    if regionLookupCache then
        return regionLookupCache
    end

    if not data or not data.icann_regions then
        regionLookupCache = {}
        return regionLookupCache
    end

    local lookup = {}
    for code, region in pairs(data.icann_regions) do
        if region.name then
            lookup[NormalizationText.normalizeText(region.name)] = code
        end
        if region.variations and type(region.variations) == "table" then
            for _, variation in pairs(region.variations) do
                lookup[NormalizationText.normalizeText(variation)] = code
            end
        end
    end

    regionLookupCache = lookup
    return lookup
end

--------------------------------------------------------------------------------
-- Core API Functions (Public Interface)
--------------------------------------------------------------------------------

local CountryData = {}

-- Load data and initialize caches
function CountryData.loadData(frame)
    return loadData()
end

-- Reset all caches (primarily for testing)
function CountryData.resetCaches()
    resetCaches()
    return true
end

-- Get country data by ISO code (Refactored to use new cache logic)
function CountryData.getCountryByCode(code)
    if not code or code == "" then
        return nil
    end
    
    local cacheKey = createCacheKey("getCountryByCode", code)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local data = loadData()
    code = code:upper() -- Standardize code
    
    local result = data and data.countries and data.countries[code] or nil
    
    functionCache[cacheKey] = result
    return result
end

-- Get country data by name (Refactored to use new cache logic)
function CountryData.getCountryByName(name)
    if not name or name == "" then
        return nil
    end

    local cacheKey = createCacheKey("getCountryByName", name)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end

    local data = loadData()
    local nameLookup = buildNameLookupCache(data)

    -- Normalize the input name in one go (text normalization includes lowercasing)
    local normalized = NormalizationText.normalizeText(name)
    
    -- First, try a direct lookup with the normalized name
    local code = nameLookup[normalized]

    -- If not found, try looking up the diacritic-stripped version
    if not code then
        local stripped = DiacriticNormalization.removeDiacritics(name)
        if stripped ~= name then
            code = nameLookup[NormalizationText.normalizeText(stripped)]
        end
    end

    local result = nil
    if code then
        result = data.countries[code]
    end

    functionCache[cacheKey] = result
    return result
end

-- Get country code by name
function CountryData.getCountryCodeByName(name)
    if not name or name == "" then
        return nil
    end
    
    local cacheKey = createCacheKey("getCountryCodeByName", name)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local country = CountryData.getCountryByName(name)
    
    -- The country object from the dataset doesn't inherently contain its own ISO code key.
    -- We must iterate through the dataset to find the key corresponding to the found country object.
    if country then
        local data = loadData()
        for code, countryData in pairs(data.countries) do
            if countryData == country then
                functionCache[cacheKey] = code
                return code
            end
        end
    end

    -- If no country was found, or no matching code was found, cache and return nil.
    functionCache[cacheKey] = nil
    return nil
end

-- (The rest of the functions remain unchanged for now, but will be updated in subsequent phases)

function CountryData.normalizeCountryName(name)
    if not name or name == "" then
        return "(Unrecognized)"
    end
    
    local cacheKey = createCacheKey("normalizeCountryName", name)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local country = CountryData.getCountryByName(name)
    
    local result
    if country and country.name then
        result = country.name
    else
        result = "(Unrecognized)"
    end
    
    functionCache[cacheKey] = result
    return result
end

function CountryData.getRegionByCountry(name)
    if not name or name == "" then
        return "(Unrecognized)"
    end
    
    local cacheKey = createCacheKey("getRegionByCountry", name)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local country = CountryData.getCountryByName(name)
    
    local result
    if country and country.icann_region then
        result = country.icann_region
    else
        result = "(Unrecognized)"
    end
    
    functionCache[cacheKey] = result
    return result
end

function CountryData.getCountriesByRegion(region)
    if not region or region == "" then
        return {}
    end
    
    local cacheKey = createCacheKey("getCountriesByRegion", region)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local data = loadData()
    local regionLookup = buildRegionLookupCache(data)
    
    local normalized = NormalizationText.normalizeText(region)
    local regionCode = regionLookup[normalized]
    
    local result = {}
    if regionCode and data.countries then
        for code, country in pairs(data.countries) do
            if country.icann_region == regionCode then
                table.insert(result, {
                    code = code,
                    name = country.name
                })
            end
        end
    end
    
    functionCache[cacheKey] = result
    return result
end

function CountryData.getAllCountryCodes()
    local cacheKey = "getAllCountryCodes"
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local data = loadData()
    local result = {}
    if data and data.countries then
        for code in pairs(data.countries) do
            table.insert(result, code)
        end
    end
    
    functionCache[cacheKey] = result
    return result
end

function CountryData.getAllCountryNames()
    local cacheKey = "getAllCountryNames"
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local data = loadData()
    local result = {}
    if data and data.countries then
        for _, country in pairs(data.countries) do
            table.insert(result, country.name)
        end
    end
    
    functionCache[cacheKey] = result
    return result
end

function CountryData.getCountryProperty(code, property)
    if not code or code == "" or not property or property == "" then
        return nil
    end
    
    local cacheKey = createCacheKey("getCountryProperty", code, property)
    if propertyCache[cacheKey] ~= nil then
        return propertyCache[cacheKey]
    end
    
    local country = CountryData.getCountryByCode(code)
    local result = country and country[property] or nil
    
    propertyCache[cacheKey] = result
    return result
end

function CountryData.getCountryPropertyByName(name, property)
    if not name or name == "" or not property or property == "" then
        return nil
    end
    
    local cacheKey = createCacheKey("getCountryPropertyByName", name, property)
    if propertyCache[cacheKey] ~= nil then
        return propertyCache[cacheKey]
    end
    
    local code = CountryData.getCountryCodeByName(name)
    local result = nil
    if code then
        result = CountryData.getCountryProperty(code, property)
    end
    
    propertyCache[cacheKey] = result
    return result
end

function CountryData.getAvailableProperties(code)
    if not code or code == "" then
        return {}
    end
    
    local cacheKey = createCacheKey("getAvailableProperties", code)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local country = CountryData.getCountryByCode(code)
    local properties = {}
    if country then
        for property in pairs(country) do
            table.insert(properties, property)
        end
    end
    
    functionCache[cacheKey] = properties
    return properties
end

function CountryData.getAllPropertyNames()
    local cacheKey = "getAllPropertyNames"
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local data = loadData()
    local properties = {}
    local seen = {}
    if data and data.countries then
        for _, country in pairs(data.countries) do
            for property in pairs(country) do
                if not seen[property] then
                    seen[property] = true
                    table.insert(properties, property)
                end
            end
        end
    end
    
    functionCache[cacheKey] = properties
    return properties
end

function CountryData.getSemanticCountryRegionProperties(countryValue)
    local properties = {}
    if not countryValue or countryValue == "" then
        return properties
    end
    
    local ConfigRepository = require('Module:ConfigRepository')
    local countryPropertyName = ConfigRepository.getSemanticPropertyName("Has country")
    local regionPropertyName = ConfigRepository.getSemanticPropertyName("Has ICANN region")
    
    if not countryPropertyName or not regionPropertyName then
        return properties
    end
    
    local countries = {}
    for country in string.gmatch(countryValue, "[^;]+") do
        local trimmedCountry = country:match("^%s*(.-)%s*$")
        if trimmedCountry and trimmedCountry ~= "" then
            table.insert(countries, trimmedCountry)
        end
    end
    
    for _, countryName in ipairs(countries) do
        local normalizedCountry = CountryData.normalizeCountryName(countryName)
        if normalizedCountry ~= "(Unrecognized)" then
            -- Initialize property tables if they don't exist
            properties[countryPropertyName] = properties[countryPropertyName] or {}
            table.insert(properties[countryPropertyName], normalizedCountry)
            
            local region = CountryData.getRegionByCountry(normalizedCountry)
            if region and region ~= "(Unrecognized)" then
                properties[regionPropertyName] = properties[regionPropertyName] or {}
                table.insert(properties[regionPropertyName], region)
            end
        end
    end
    
    return properties
end

function CountryData.exportAsJson()
    local data = loadData()
    if not data or not data.countries then
        return '{}'
    end
    
    if mw.text and mw.text.jsonEncode then
        local success, result = pcall(function()
            return mw.text.jsonEncode(data)
        end)
        if success and result then
            return result
        end
    end
    return '{}'
end

local function getRegionClass(region)
    if not region or region == "(Unrecognized)" then
        return "region-default"
    end
    if region == "NA" or region == "LAC" then
        return "region-americas"
    elseif region == "AP" then
        return "region-asia-pacific"
    else
        return "region-europe-africa"
    end
end

function CountryData.formatCountryList(value)
    if not value or value == "" then return "" end

    local ListGeneration = require('Module:ListGeneration')
    local itemsToProcess = {}

    -- First, check if the entire string is a single, valid country.
    -- This correctly handles names like "Trinidad and Tobago".
    local singleCountryName = CountryData.normalizeCountryName(value)
    if singleCountryName ~= "(Unrecognized)" then
        -- If it's a valid country, treat it as a single item.
        table.insert(itemsToProcess, value)
    else
        -- If not a single country, assume it's a list and split ONLY by semicolon.
        -- This is safer than letting ListGeneration guess the delimiter.
        for item in string.gmatch(value, "[^;]+") do
            local trimmed = item:match("^%s*(.-)%s*$")
            if trimmed and trimmed ~= "" then
                table.insert(itemsToProcess, trimmed)
            end
        end
    end

    -- Define the item hook for country-specific formatting
    local function countryItemHook(countryName)
        local normalized = CountryData.normalizeCountryName(countryName)
        if normalized ~= "(Unrecognized)" then
            local countryRegion = CountryData.getRegionByCountry(normalized)
            -- Return a table with content and class for the li element
            return {
                content = normalized,
                class = getRegionClass(countryRegion)
            }
        end
        return nil -- Exclude unrecognized countries from the list
    end

    -- Set the options for the list generation
    local options = {
        mode = 'bullet',
        listClass = 'template-list-country',
        itemHook = countryItemHook
    }

    -- Pass the pre-processed table of items to the list generator.
    return ListGeneration.createList(itemsToProcess, options)
end

function CountryData.formatCountries(value)
    return CountryData.formatCountryList(value)
end

function CountryData.getCountriesForCategories(value)
    if not value or value == "" then return {} end
    
    local countries = {}
    for countryName in string.gmatch(value, "[^;]+") do
        local trimmed = countryName:match("^%s*(.-)%s*$")
        if trimmed and trimmed ~= "" then
            local normalized = CountryData.normalizeCountryName(trimmed)
            if normalized ~= "(Unrecognized)" then
                table.insert(countries, normalized)
            end
        end
    end
    
    return countries
end

function CountryData.getFlagFileName(countryNameOrCode)
    if not countryNameOrCode or countryNameOrCode == '' then return nil end
    
    local inputName = countryNameOrCode:gsub('_', ' ')
    local isoCode
    
    -- First, try to get the ISO code by treating inputName as a country name.
    isoCode = CountryData.getCountryCodeByName(inputName) 
    
    -- If no code was found by name, and the inputName itself is 2 characters long,
    -- it might be an ISO code already. Let's validate it.
    if not isoCode and #inputName == 2 then
        if CountryData.getCountryByCode(inputName) then 
            isoCode = inputName:upper()
        end
    end
    
    if not isoCode or #isoCode ~= 2 then return nil end

    return 'Flag-' .. string.lower(isoCode) .. '.svg' 
end

return CountryData