Module:number list: Difference between revisions

No edit summary
No edit summary
Line 1: Line 1:
-- TODO: support <id:...>. currently it does nothing.
local m_links = require("Module:links")
local m_links = require("Module:links")
local m_str_utils = require("Module:string utilities")
local m_str_utils = require("Module:string utilities")
Line 10: Line 8:
local list_to_set = require("Module:table").listToSet
local list_to_set = require("Module:table").listToSet
local sort = table.sort
local sort = table.sort
local split = m_str_utils.split
local u = m_str_utils.char
local u = m_str_utils.char
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
Line 15: Line 14:


local export = {}
local export = {}
local decimal_strategy


--[=[
--[=[
Line 34: Line 34:


local default_form_types = {
local default_form_types = {
{key = "cardinal", display = "[[w:cardinal number|Cardinal]]"},
{key = "cardinal", display = "[[wikt:cardinal number|Cardinal]]"},
{key = "ordinal", display = "[[w:ordinal number|Ordinal]]"},
{key = "ordinal", display = "[[wikt:ordinal number|Ordinal]]"},
{key = "ordinal_abbr", display = "[[w:ordinal number|Ordinal]] [[w:abbreviation]]"},
{key = "ordinal_abbr", display = "[[wikt:ordinal number|Ordinal]] [[wikt:abbreviation|abbreviation]]"},
{key = "adverbial", display = "[[w:adverbial number|Adverbial]]"},
{key = "adverbial", display = "[[wikt:adverbial number|Adverbial]]"},
{key = "multiplier", display = "[[w:multiplier|Multiplier]]"},
{key = "multiplier", display = "[[wikt:multiplier|Multiplier]]"},
{key = "distributive", display = "[[w:distributive number|Distributive]]"},
{key = "distributive", display = "[[wikt:distributive number|Distributive]]"},
{key = "collective", display = "[[w:collective number|Collective]]"},
{key = "collective", display = "[[wikt:collective number|Collective]]"},
{key = "fractional", display = "[[w:fractional|Fractional]]"},
{key = "fractional", display = "[[wikt:fractional|Fractional]]"},
}
}


Line 55: Line 55:
lower = true,
lower = true,
}
}
local function track(page)
require("Module:debug/track")("number list/" .. page)
return true
end


--[=[
--[=[
Line 91: Line 96:
end
end
return intersection
return intersection
end
-- Count keys in a set table (never use `#` on these; it is not the set cardinality).
local function set_size(set)
local n = 0
for _ in pairs(set) do
n = n + 1
end
return n
end
end


Line 109: Line 123:
-- Parse a form with modifiers such as 'vuitanta-vuit<tag:Central>' or 'سیزده<tr:sizdah>'
-- Parse a form with modifiers such as 'vuitanta-vuit<tag:Central>' or 'سیزده<tr:sizdah>'
-- or 'سیزده<tr:sizdah><tag:Iranian>' into its component parts. Return a form object, i.e. an object with fields
-- or 'سیزده<tr:sizdah><tag:Iranian>' into its component parts. Return a form object, i.e. an object with fields
-- `form` for the form, and `tr`, `tag`, `q`, `qq` or `link` for the modifiers. The `tag` field is a tag list
-- `form` for the form, and `tr`, `tag`, `q`, `qq`, `g` or `link` for the modifiers. The `tag` field is a tag list
-- (see above).
-- (see above).
function export.parse_form_and_modifiers(form_with_modifiers)
function export.parse_form_and_modifiers(form_with_modifiers)
Line 130: Line 144:
retval.tag = {content}
retval.tag = {content}
end
end
elseif prefix == "q" or prefix == "qq" or prefix == "tr" or prefix == "link" or prefix == "id" or prefix == "alt" then
elseif prefix == "q" or prefix == "qq" or prefix == "tr" or prefix == "link" or prefix == "id" or prefix == "g" or prefix == "alt" then
if retval[prefix] then
if retval[prefix] then
error(("Duplicate modifier '%s' in data module form, already saw value '%s': %s"):format(prefix,
error(("Duplicate modifier '%s' in data module form, already saw value '%s': %s"):format(prefix,
Line 170: Line 184:
function export.numbers_greater_than(a, b)
function export.numbers_greater_than(a, b)
return export.numbers_less_than(b, a)
return export.numbers_less_than(b, a)
end
local POSITIONAL_DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
local MAX_SAFE_INTEGER = 9007199254740991
local function get_digit_maps(base, digit_alphabet)
digit_alphabet = digit_alphabet or POSITIONAL_DIGITS
if #digit_alphabet < base then
error(("Number system base %s exceeds available digits in digit alphabet"):format(base))
end
local digit_to_value = {}
local value_to_digit = {}
for i = 1, base do
local digit = digit_alphabet:sub(i, i)
digit_to_value[digit] = i - 1
value_to_digit[i - 1] = digit
end
return digit_to_value, value_to_digit
end
local function decimal_to_base(num, base, value_to_digit)
if num == 0 then
return value_to_digit[0]
end
local parts = {}
while num > 0 do
insert(parts, 1, value_to_digit[num % base])
num = math.floor(num / base)
end
return concat(parts)
end
local function parse_positional_to_number(key, base, digit_to_value)
local n = 0
for i = 1, #key do
local value = digit_to_value[key:sub(i, i)]
if not value then
return nil
end
n = n * base + value
if n > MAX_SAFE_INTEGER then
return nil
end
end
return n
end
local function safe_integer_power(base, exp)
local result = 1
for _ = 1, exp do
if result > MAX_SAFE_INTEGER / base then
return nil
end
result = result * base
end
return result
end
local function normalize_positional_key(raw_number, opts)
local base = opts.base
local case_insensitive = opts.case_insensitive
local digit_to_value = opts.digit_to_value
local value_to_digit = opts.value_to_digit
local strip_separator = opts.strip_separator
local interpret_plain_decimal = opts.interpret_plain_decimal
local zero_digit = opts.zero_digit
local key
if type(raw_number) == "number" then
if raw_number < 0 or raw_number % 1 ~= 0 then
error(("Non-negative integer expected for positional number system, got '%s'"):format(raw_number))
end
key = decimal_to_base(raw_number, base, value_to_digit)
else
key = tostring(raw_number)
if strip_separator and strip_separator ~= "" then
key = gsub(key, strip_separator, "")
end
-- For compatibility with existing data/modules, plain decimal-digit *input* can be interpreted
-- as decimal and then converted into the configured positional key space.
if interpret_plain_decimal and key:find("^%d+$") then
key = decimal_to_base(tonumber(key), base, value_to_digit)
end
end
if case_insensitive then
key = upper(key)
end
if key == "" then
return zero_digit
end
for i = 1, #key do
local digit = key:sub(i, i)
if not digit_to_value[digit] then
error(("Extraneous characters in number: '%s'"):format(key))
end
end
key = key:gsub("^" .. zero_digit .. "+", "")
return key == "" and zero_digit or key
end
end


Line 191: Line 307:


-- Convert the given number form (taken from the data for `lang`, after parsing the form for modifiers and stripping
-- Convert the given number form (taken from the data for `lang`, after parsing the form for modifiers and stripping
-- the modifiers) to an entry name. The form may have links and/or accent/length marks that need to be stripped.
-- the modifiers) to the stripped-text version of the form. The form may have links and/or accent/length marks that need
local function form_to_entry_name(form, lang)
-- to be stripped.
return (lang:makeEntryName(m_links.remove_links(form)))
local function form_to_stripped_form(form, lang)
return lang:stripDiacritics(m_links.remove_links(form))
end
end


Line 205: Line 322:
return true
return true
end
end
local entry_name = form_to_entry_name(formobj.form, lang)
local stripped_form = form_to_stripped_form(formobj.form, lang)
return entry_name == pagename or maybe_unaffix(m_data, entry_name) == pagename
if stripped_form == pagename or maybe_unaffix(m_data, stripped_form) == pagename then
return true
end
if formobj.alt then
local stripped_alt = form_to_stripped_form(formobj.alt, lang)
if stripped_alt == pagename or maybe_unaffix(m_data, stripped_alt) == pagename then
return true
end
end
return false
end
end


-- Given the data for a language and a number (which should be in string representation), find the next and previous
-- Given the data for a language and a number (which should be in string representation), find the next and previous
-- numbers to display (in string representation).
-- numbers to display (in string representation).
local function get_next_and_prev_keys(m_data, numstr)
local function get_next_and_prev_keys(m_data, numstr, strategy, lookup_data)
local numdata = export.lookup_data(m_data, numstr)
local numdata = lookup_data(numstr)
if not numdata then
if not numdata then
return nil, nil
return nil, nil
Line 221: Line 347:
-- Find the next/previous numbers by sorting all the keys and locating the number in question among them.
-- Find the next/previous numbers by sorting all the keys and locating the number in question among them.
local sorted_list = {}
local sorted_list = {}
local seen = {}
local index = 1
local index = 1
for key, _ in pairs(m_data.numbers) do
for key, _ in pairs(m_data.numbers) do
sorted_list[index] = key
local normalized_key = strategy.normalize_data_key(key)
index = index + 1
if not seen[normalized_key] then
seen[normalized_key] = true
sorted_list[index] = normalized_key
index = index + 1
end
end
end


sort(sorted_list, export.numbers_less_than)
sort(sorted_list, strategy.compare_keys)


-- We could binary search to save time, but given that we already sort, which is supra-linear, it won't
-- We could binary search to save time, but given that we already sort, which is supra-linear, it won't
-- matter to search linearly.
-- matter to search linearly.
for i, key in ipairs(sorted_list) do
for i, key in ipairs(sorted_list) do
if export.format_fixed(key) == numstr then
if key == numstr then
nextnum = nextnum or sorted_list[i + 1]
nextnum = nextnum or sorted_list[i + 1]
prevnum = prevnum or sorted_list[i - 1]
prevnum = prevnum or sorted_list[i - 1]
Line 241: Line 372:


if nextnum then
if nextnum then
nextnum = export.format_fixed(nextnum)
nextnum = strategy.normalize_data_key(nextnum)
end
end
if prevnum then
if prevnum then
prevnum = export.format_fixed(prevnum)
prevnum = strategy.normalize_data_key(prevnum)
end
end


Line 433: Line 564:
return fixed .. " (" .. scientific .. ")"
return fixed .. " (" .. scientific .. ")"
end
end
end
local function derive_related_numbers_decimal(cur_num, cur_data, next_num, prev_num, lookup_data)
local k, m
if cur_num == "0" then
k = 0
m = 1
else
local kstr, mstr = cur_num:match("^([0-9]*[1-9])(0*)$")
if not kstr then
error("Internal error: Unable to match number '" .. cur_num .. "'")
elseif #kstr > 15 then
error("Can't handle number with more than 15 digits before the trailing zeros: '" .. cur_num .. "'")
end
k = tonumber(kstr)
m = #mstr
end
local function make_greater_power_of_ten(power)
return cur_num .. ("0"):rep(power)
end
local function make_lesser_power_of_ten(power)
local desired_zeros = m - power
if desired_zeros < 0 then
return nil
end
return k .. ("0"):rep(desired_zeros)
end
local next_outer_data, prev_outer_data
local next_outer_num, prev_outer_num = cur_data.next_outer, cur_data.prev_outer
local power_of_10_sequence = { 1, 3, 2, 6 }
if next_outer_num then
next_outer_data = lookup_data(next_outer_num, "next outer")
else
local function try(num)
local data = (not next_num or export.numbers_greater_than(num, next_num)) and lookup_data(num) or nil
if data then
next_outer_num = num
next_outer_data = data
end
return data
end
if not try((k + 1) .. ("0"):rep(m)) and k == 1 then
for _, power_of_10 in ipairs(power_of_10_sequence) do
if try(make_greater_power_of_ten(power_of_10)) then
break
end
end
end
end
if prev_outer_num then
prev_outer_data = lookup_data(prev_outer_num, "previous outer")
else
local function try(num)
local data = (not prev_num or export.numbers_less_than(num, prev_num)) and lookup_data(num) or nil
if data then
prev_outer_num = num
prev_outer_data = data
end
return data
end
if not (k == 0 or m == 0) then
local num_to_try
if k == 1 then
num_to_try = "9" .. ("0"):rep(m - 1)
else
num_to_try = (k - 1) .. ("0"):rep(m)
end
if not try(num_to_try) and k == 1 then
for _, power_of_10 in ipairs(power_of_10_sequence) do
local power_num_to_try = make_lesser_power_of_ten(power_of_10)
if power_num_to_try and try(power_num_to_try) then
break
end
end
end
end
end
local upper_data, lower_data
local upper_num, lower_num = cur_data.upper, cur_data.lower
if upper_num then
upper_data = lookup_data(upper_num, "upper")
else
upper_num = make_greater_power_of_ten(1)
if upper_num == next_num or cur_num == "0" then
upper_num = nil
else
upper_data = lookup_data(upper_num)
end
end
if lower_num then
lower_data = lookup_data(lower_num, "lower")
elseif not (k == 0 or m == 0) then
lower_num = make_lesser_power_of_ten(1)
if lower_num == prev_num then
lower_num = nil
else
lower_data = lookup_data(lower_num)
end
end
return next_outer_num, next_outer_data, prev_outer_num, prev_outer_data, upper_num, upper_data, lower_num, lower_data
end
local function derive_related_numbers_positional(strategy, cur_num, cur_data, next_num, prev_num, lookup_data)
local zero_digit = strategy.zero_digit
local base = strategy.base
local power_base = strategy.power_base
local value_to_digit = strategy.value_to_digit
local digit_to_value = strategy.digit_to_value
local power_sequence = strategy.power_sequence
local significant = cur_num:gsub(zero_digit .. "+$", "")
local m = #cur_num - #significant
local kstr = significant == "" and zero_digit or significant
local k_value = parse_positional_to_number(kstr, base, digit_to_value)
local next_outer_data, prev_outer_data
local next_outer_num, prev_outer_num = cur_data.next_outer, cur_data.prev_outer
local function value_to_key(value)
return decimal_to_base(value, base, value_to_digit)
end
local function make_greater_power(power)
if not k_value then
return nil
end
local factor = safe_integer_power(power_base, power)
if not factor then
return nil
end
local cur_value = parse_positional_to_number(cur_num, base, digit_to_value)
if not cur_value or cur_value > MAX_SAFE_INTEGER / factor then
return nil
end
return value_to_key(cur_value * factor)
end
local function make_lesser_power(power)
local factor = safe_integer_power(power_base, power)
if not factor then
return nil
end
local cur_value = parse_positional_to_number(cur_num, base, digit_to_value)
if not cur_value or cur_value % factor ~= 0 then
return nil
end
return value_to_key(cur_value / factor)
end
if next_outer_num then
next_outer_data = lookup_data(next_outer_num, "next outer")
else
local function try(num)
local data = (not next_num or strategy.compare_keys(next_num, num)) and lookup_data(num) or nil
if data then
next_outer_num = num
next_outer_data = data
end
return data
end
if k_value then
if not try(decimal_to_base(k_value + 1, base, value_to_digit) .. zero_digit:rep(m)) and k_value == 1 then
for _, power in ipairs(power_sequence) do
if try(make_greater_power(power)) then
break
end
end
end
end
end
if prev_outer_num then
prev_outer_data = lookup_data(prev_outer_num, "previous outer")
else
local function try(num)
local data = (not prev_num or strategy.compare_keys(num, prev_num)) and lookup_data(num) or nil
if data then
prev_outer_num = num
prev_outer_data = data
end
return data
end
if not (cur_num == zero_digit or m == 0) and k_value then
local num_to_try
if k_value == 1 then
num_to_try = value_to_digit[base - 1] .. zero_digit:rep(m - 1)
else
num_to_try = decimal_to_base(k_value - 1, base, value_to_digit) .. zero_digit:rep(m)
end
if not try(num_to_try) and k_value == 1 then
for _, power in ipairs(power_sequence) do
local power_num_to_try = make_lesser_power(power)
if power_num_to_try and try(power_num_to_try) then
break
end
end
end
end
end
local upper_data, lower_data
local upper_num, lower_num = cur_data.upper, cur_data.lower
if upper_num then
upper_data = lookup_data(upper_num, "upper")
else
upper_num = make_greater_power(1)
if upper_num == next_num or cur_num == zero_digit then
upper_num = nil
else
upper_data = lookup_data(upper_num)
end
end
if lower_num then
lower_data = lookup_data(lower_num, "lower")
elseif not (cur_num == zero_digit or m == 0) then
lower_num = make_lesser_power(1)
if lower_num == prev_num then
lower_num = nil
else
lower_data = lookup_data(lower_num)
end
end
return next_outer_num, next_outer_data, prev_outer_num, prev_outer_data, upper_num, upper_data, lower_num, lower_data
end
local function create_positional_strategy(config)
local base = config.base
local digit_alphabet = config.digit_alphabet or POSITIONAL_DIGITS
local case_insensitive = config.case_insensitive
if case_insensitive == nil then
case_insensitive = base <= 36
end
local strip_separator = config.strip_separator or ","
local digit_to_value, value_to_digit = get_digit_maps(base, digit_alphabet)
local zero_digit = value_to_digit[0]
local display_separator = config.display_separator
local display_group = config.display_group
local display_group_start = config.display_group_start
local display_indic = config.display_indic
local display_keys_as_decimal = config.display_keys_as_decimal
if display_keys_as_decimal == nil then
display_keys_as_decimal = true
end
local power_sequence = config.power_sequence or {1, 3, 2, 6}
local power_base = config.power_base or 10
local strategy = {
id = config.id or ("base" .. base),
base = base,
power_base = power_base,
digit_to_value = digit_to_value,
value_to_digit = value_to_digit,
zero_digit = zero_digit,
power_sequence = power_sequence,
}
function strategy.normalize_data_key(raw_number)
return normalize_positional_key(raw_number, {
base = base,
case_insensitive = case_insensitive,
digit_to_value = digit_to_value,
value_to_digit = value_to_digit,
strip_separator = nil,
interpret_plain_decimal = false,
zero_digit = zero_digit,
})
end
function strategy.normalize_input(raw_number)
return normalize_positional_key(raw_number, {
base = base,
case_insensitive = case_insensitive,
digit_to_value = digit_to_value,
value_to_digit = value_to_digit,
strip_separator = strip_separator,
interpret_plain_decimal = true,
zero_digit = zero_digit,
})
end
function strategy.normalize_input_candidates(raw_number)
local key_as_system = normalize_positional_key(raw_number, {
base = base,
case_insensitive = case_insensitive,
digit_to_value = digit_to_value,
value_to_digit = value_to_digit,
strip_separator = strip_separator,
interpret_plain_decimal = false,
zero_digit = zero_digit,
})
local key_as_decimal = normalize_positional_key(raw_number, {
base = base,
case_insensitive = case_insensitive,
digit_to_value = digit_to_value,
value_to_digit = value_to_digit,
strip_separator = strip_separator,
interpret_plain_decimal = true,
zero_digit = zero_digit,
})
if key_as_system == key_as_decimal then
return {key_as_system}
end
return {key_as_system, key_as_decimal}
end
function strategy.lookup_data(m_data, key)
key = strategy.normalize_data_key(key)
local direct = m_data.numbers[key]
if direct then
return direct
end
local parsed = parse_positional_to_number(key, base, digit_to_value)
return parsed and m_data.numbers[parsed] or nil
end
function strategy.compare_keys(a, b)
a = strategy.normalize_data_key(a)
b = strategy.normalize_data_key(b)
if #a ~= #b then
return #a < #b
end
return a < b
end
function strategy.format_key_for_display(key)
key = strategy.normalize_data_key(key)
if display_keys_as_decimal then
local decimal_value = parse_positional_to_number(key, base, digit_to_value)
if decimal_value then
return export.format_number_for_display(decimal_value)
end
end
if display_separator then
if display_indic then
return add_separator(key, display_separator, 2, 3)
end
return add_separator(key, display_separator, display_group or 3, display_group_start)
end
return key
end
function strategy.derive_related_numbers(cur_num, cur_data, next_num, prev_num, lookup_data)
return derive_related_numbers_positional(strategy, cur_num, cur_data, next_num, prev_num, lookup_data)
end
return strategy
end
decimal_strategy = {
id = "decimal",
base = 10,
zero_digit = "0",
power_sequence = {1, 3, 2, 6},
}
function decimal_strategy.normalize_data_key(raw_number)
return export.format_fixed(raw_number)
end
function decimal_strategy.normalize_input(raw_number)
local normalized = tostring(raw_number):gsub(",", "")
if not normalized:find("^%d+$") then
error("Extraneous characters in parameter 2: should be decimal number (integer): '" .. normalized .. "'")
end
return normalized
end
function decimal_strategy.normalize_input_candidates(raw_number)
return {decimal_strategy.normalize_input(raw_number)}
end
function decimal_strategy.lookup_data(m_data, key)
return export.lookup_data(m_data, key)
end
function decimal_strategy.compare_keys(a, b)
return export.numbers_less_than(a, b)
end
function decimal_strategy.format_key_for_display(key)
return export.format_number_for_display(key)
end
function decimal_strategy.derive_related_numbers(cur_num, cur_data, next_num, prev_num, lookup_data)
return derive_related_numbers_decimal(cur_num, cur_data, next_num, prev_num, lookup_data)
end
local function resolve_number_system(m_data)
local config = m_data.number_system
if not config then
return decimal_strategy
end
if type(config) == "string" then
config = {id = config}
end
if config.id == "decimal" then
return decimal_strategy
end
if config.id == "base20" then
config.base = config.base or 20
elseif config.id == "base60" then
config.base = config.base or 60
elseif config.id == "positional" then
-- base should be explicitly given or derived below.
elseif config.id == "custom" then
if not (config.module and config.func) then
error("custom number_system requires both module and func")
end
local custom_strategy = require("Module:" .. config.module)[config.func](config)
return custom_strategy
end
config.base = config.base or tonumber(config.id and config.id:match("^base(%d+)$"))
if not config.base then
error("Unsupported number_system id '" .. tostring(config.id) .. "'")
end
if config.base < 2 then
error("number_system base must be >= 2")
end
return create_positional_strategy(config)
end
end


Line 474: Line 1,036:
function export.format_formobj(formobj, m_data, lang)
function export.format_formobj(formobj, m_data, lang)
local left_q = formobj.q and require("Module:qualifier").format_qualifier(formobj.q) .. " " or ""
local left_q = formobj.q and require("Module:qualifier").format_qualifier(formobj.q) .. " " or ""
local right_q = formobj.qq and " " .. require("Module:qualifier").format_qualifier(formobj.qq) or ""
local right_q = ((formobj.g and " " .. require("Module:gender and number").format_genders(split(formobj.g, ",")) or "")
.. (formobj.qq and " " .. require("Module:qualifier").format_qualifier(formobj.qq) or ""))
local term = maybe_unaffix(m_data, formobj.form)
local term = maybe_unaffix(m_data, formobj.form)
local alt = nil
local alt = formobj.alt
if term ~= formobj.form then alt = formobj.form end
if not alt and term ~= formobj.form then
alt = formobj.form
end
return left_q .. m_links.full_link{
return left_q .. m_links.full_link{
lang = lang, term = term, alt = alt, tr = formobj.tr,
lang = lang, term = term, alt = alt, tr = formobj.tr, id = formobj.id,
} .. right_q
} .. right_q
end
end
Line 495: Line 1,060:


local parent_args = frame:getParent().args
local parent_args = frame:getParent().args
if parent_args.pagename then
track("show-box-pagename")
end
local args = require("Module:parameters").process(parent_args, params, nil, "number list", "show_box")
local args = require("Module:parameters").process(parent_args, params, nil, "number list", "show_box")


Line 504: Line 1,072:
local module_name = export.get_data_module_name(langcode)
local module_name = export.get_data_module_name(langcode)
local m_data = require(module_name)
local m_data = require(module_name)
local number_system = resolve_number_system(m_data)


local pagename = args.pagename or (mw.title.getCurrentTitle().nsText == "Reconstruction" and "*" or "") .. mw.title.getCurrentTitle().subpageText
local pagename = args.pagename or (mw.title.getCurrentTitle().nsText == "Reconstruction" and "*" or "") .. mw.loadData("Module:headword/data").pagename
-- Resolve any risky characters which makeEntryName will escape, so that any matches involving them work correctly.
pagename = (lang:makeEntryName(pagename))
local cur_type = args.type
local cur_type = args.type


Line 532: Line 1,099:
for _, num_and_type in ipairs(nums_and_types) do
for _, num_and_type in ipairs(nums_and_types) do
local num = num_and_type[1]
local num = num_and_type[1]
num = export.format_fixed(num)
num = number_system.normalize_data_key(num)
if cur_num and num ~= cur_num then
if cur_num and num ~= cur_num then
local errparts = {}
local errparts = {}
for _, num_and_type in ipairs(nums_and_types) do
for _, num_and_type in ipairs(nums_and_types) do
local num, typ = unpack(num_and_type)
local num, typ = unpack(num_and_type)
num = number_system.normalize_data_key(num)
insert(errparts, ("%s (%s)"):format(num, typ))
insert(errparts, ("%s (%s)"):format(num, typ))
end
end
Line 547: Line 1,115:
end
end


cur_num = cur_num:gsub(",", "") -- remove thousands separators
local function candidate_matches_pagename(candidate_num)
if not cur_num:find("^%d+$") then
local candidate_data = number_system.lookup_data(m_data, candidate_num)
error("Extraneous characters in parameter 2: should be decimal number (integer): '" .. cur_num .. "'")
if not candidate_data then
return false
end
for numtype, forms in pairs(candidate_data) do
if not non_form_types[numtype] and (not cur_type or numtype == cur_type) then
local form_list = type(forms) == "table" and forms or {forms}
for _, form in ipairs(form_list) do
local formobj = export.parse_form_and_modifiers(form)
if form_equals_pagename(formobj, pagename, m_data, lang) then
return true
end
end
end
end
return false
end
 
local cur_num_candidates = number_system.normalize_input_candidates and
number_system.normalize_input_candidates(cur_num) or
{number_system.normalize_input(cur_num)}
cur_num = cur_num_candidates[1]
if #cur_num_candidates > 1 then
for _, candidate in ipairs(cur_num_candidates) do
if candidate_matches_pagename(candidate) then
cur_num = candidate
break
end
end
end
end


Line 555: Line 1,150:
-- param_for_error is given).
-- param_for_error is given).
local function lookup_data(numstr, param_for_error)
local function lookup_data(numstr, param_for_error)
local retval = export.lookup_data(m_data, numstr)
numstr = number_system.normalize_data_key(numstr)
local retval = number_system.lookup_data(m_data, numstr)
if not retval and param_for_error then
if not retval and param_for_error then
error(('The %s number "%s" specified in the "numbers" table entry for "%s" cannot be found in '
error(('The %s number "%s" specified in the "numbers" table entry for "%s" cannot be found in '
Line 667: Line 1,263:
local common1 = set_intersection(cur_tag_set, list_to_set(tag_list1))
local common1 = set_intersection(cur_tag_set, list_to_set(tag_list1))
local common2 = set_intersection(cur_tag_set, list_to_set(tag_list2))
local common2 = set_intersection(cur_tag_set, list_to_set(tag_list2))
if #common1 ~= #common2 then
local n_common1, n_common2 = set_size(common1), set_size(common2)
return #common1 < #common2
if n_common1 ~= n_common2 then
return n_common1 > n_common2 -- larger overlap with current tag list first
end
end
-- Then compare inversely by number of tags not in common with the current tag list (which is equivalent to
-- When overlap ties, shorter tag lists first (untagged default before explicit <tag:...> rows).
-- comparing by total number of tags, since tags should be distinct).
if #tag_list1 ~= #tag_list2 then
if #tag_list1 ~= #tag_list2 then
return #tag_list1 > #tag_list2
return #tag_list1 < #tag_list2
end
end
-- Finally, compare by the original ordering in the number data, but if a tag is the same as the current
-- Finally, compare by the original ordering in the number data, but if a tag is the same as the current
Line 721: Line 1,317:


-- Current number in header
-- Current number in header
local cur_display = export.format_number_for_display(cur_num)
local cur_display = number_system.format_key_for_display(cur_num)


local numeral
local numeral
Line 727: Line 1,323:
numeral = export.generate_non_arabic_numeral(m_data.numeral_config, cur_num)
numeral = export.generate_non_arabic_numeral(m_data.numeral_config, cur_num)
elseif cur_data["numeral"] then
elseif cur_data["numeral"] then
numeral = export.format_fixed(cur_data["numeral"])
numeral = number_system.normalize_data_key(cur_data["numeral"])
end
end


Line 747: Line 1,343:
--    question, number not considering a number if it's the same as the next/previous number.
--    question, number not considering a number if it's the same as the next/previous number.


local next_num, prev_num = get_next_and_prev_keys(m_data, cur_num)
local next_num, prev_num = get_next_and_prev_keys(m_data, cur_num, number_system, lookup_data)
local next_data = next_num and lookup_data(next_num, "next")
local next_data = next_num and lookup_data(next_num, "next")
local prev_data = prev_num and lookup_data(prev_num, "previous")
local prev_data = prev_num and lookup_data(prev_num, "previous")
 
local next_outer_num, next_outer_data, prev_outer_num, prev_outer_data, upper_num, upper_data, lower_num, lower_data =
--------- Decompose number into mantissa (k) and exponent (m). ----------
number_system.derive_related_numbers(cur_num, cur_data, next_num, prev_num, lookup_data)
 
local k, m
if cur_num == "0" then
k = 0
m = 1
else
local kstr, mstr = cur_num:match("^([0-9]*[1-9])(0*)$")
if not kstr then
error("Internal error: Unable to match number '" .. cur_num .. "'")
elseif #kstr > 15 then
-- This is because some numbers with 16 or more digits can't be represented exactly.
error("Can't handle number with more than 15 digits before the trailing zeros: '" .. cur_num .. "'")
end
k = tonumber(kstr)
m = #mstr
end
 
-- Find the next greater power of 10 for cur_num, up to 10^6. `try` should look up the data for a power of 10
-- and return it if it's available and the number passes any checks, otherwise nil.
local function make_greater_power_of_ten(power)
return cur_num .. ("0"):rep(power)
end
 
-- Find the next lesser power of 10 for cur_num, up to 10^6. `try` should look up the data for a power of 10
-- and return it if it's available and the number passes any checks, otherwise nil.
local function make_lesser_power_of_ten(power)
local desired_zeros = m - power
if desired_zeros < 0 then
return nil
end
return k .. ("0"):rep(desired_zeros)
end
 
 
local next_outer_data, prev_outer_data
local next_outer_num, prev_outer_num = cur_data.next_outer, cur_data.prev_outer
 
-- When trying to find then next/previous outer numbers, first, if the base-10 mantissa is not 1 or 0, we add 1 to
-- or subtract 1 from the mantissa, keeping the same number of zeros. Hence, for 300, we try 400 for the next outer,
-- 200 for the previous outer. For 900, we try 1000 for the next outer and 800 for the previous outer. If the
-- mantissa is 1, the next outer is computed the same but for the previous outer we use 9 followed by one fewer
-- zero. Hence, for 100 we try 200 for the next outer but 90 for the previous outer. If the mantissa is 0 (i.e. the
-- entire number is 0), we try 10 for the next outer, and have no previous outer.
--
-- Next, if the number is an even power of 10, we try 10x, 1000x greater, 100x greater and 1,000,000x greater, in
-- that sequence. Essentially, first we try the next power of 10; then we try the next short-scale number (billion,
-- trillion, etc. where large numbers follow a 10^3 sequence); then we try the next long-scale number (where large
-- numbers follow a 10^6 sequence); then we try the next Indic-scale number (where large numbers follow a 10^2
-- sequence: lakh, crore, arab, ...). We don't just try powers of 10 in order because then if e.g. we have entries
-- for one million, ten million, one hundred million and one billion, and the current number is one million, the
-- next number will be ten million and the next outer number one hundred million, when it would be cleaner to have
-- one billion as the outer number (and in many cases, there is no Wiktionary entry for one hundred million).
--
-- For the previous outer number, we do an analogous algorithm but make sure we don't try numbers less than 1.
local power_of_10_sequence = { 1, 3, 2, 6 }
 
--------- Determine next outer number. ----------
if next_outer_num then
next_outer_data = lookup_data(next_outer_num, "next outer")
else
local function try(num)
local data = (not next_num or export.numbers_greater_than(num, next_num)) and lookup_data(num) or nil
if data then
next_outer_num = num
next_outer_data = data
end
return data
end
if not try((k + 1) .. ("0"):rep(m)) and k == 1 then
-- Try looking up a greater power of ten instead.
for _, power_of_10 in ipairs(power_of_10_sequence) do
if try(make_greater_power_of_ten(power_of_10)) then
break
end
end
end
end
 
--------- Determine previous outer number. ----------
if prev_outer_num then
prev_outer_data = lookup_data(prev_outer_num, "previous outer")
else
local function try(num)
local data = (not prev_num or export.numbers_less_than(num, prev_num)) and lookup_data(num) or nil
if data then
prev_outer_num = num
prev_outer_data = data
end
return data
end
if k == 0 or m == 0 then
-- less than 10; no previous outer num
else
local num_to_try
if k == 1 then
num_to_try = "9" .. ("0"):rep(m - 1)
else
num_to_try = (k - 1) .. ("0"):rep(m)
end
if not try(num_to_try) and k == 1 then
-- Try looking up a smaller power of ten instead.
for _, power_of_10 in ipairs(power_of_10_sequence) do
local num_to_try = make_lesser_power_of_ten(power_of_10)
if num_to_try and try(num_to_try) then
break
end
end
end
end
end
 
local upper_data, lower_data
local upper_num, lower_num = cur_data.upper, cur_data.lower
 
--------- Determine upper number. ----------
if upper_num then
upper_data = lookup_data(upper_num, "upper")
else
-- Try looking up the next power of ten.
upper_num = make_greater_power_of_ten(1)
if upper_num == next_num or cur_num == "0" then
upper_num = nil
else
upper_data = lookup_data(upper_num)
end
end
 
--------- Determine lower number. ----------
if lower_num then
lower_data = lookup_data(lower_num, "lower")
elseif k == 0 or m == 0 then
-- less than 10; no lower num
else
-- Try looking up the previous power or 10.
lower_num = make_lesser_power_of_ten(1)
if lower_num == prev_num then
lower_num = nil
else
lower_data = lookup_data(lower_num)
end
end


-- For a number `num` (an "adjacent" number to the current number, i.e. either next, previous, next/previous outer,
-- For a number `num` (an "adjacent" number to the current number, i.e. either next, previous, next/previous outer,
Line 941: Line 1,396:
for i, form_to_display in ipairs(forms_to_display) do
for i, form_to_display in ipairs(forms_to_display) do
forms_to_display[i] = form_to_display.link or maybe_unaffix(m_data,
forms_to_display[i] = form_to_display.link or maybe_unaffix(m_data,
form_to_entry_name(form_to_display.form, lang))
form_to_stripped_form(form_to_display.form, lang))
end
end


Line 957: Line 1,412:
end
end


num = export.format_number_for_display(num)
num = number_system.format_key_for_display(num)
local num_arrow =
local num_arrow =
arrow == "rarrow" and num .. "&nbsp;&nbsp;→&nbsp;" or
arrow == "rarrow" and num .. "&nbsp;&nbsp;→&nbsp;" or
Line 995: Line 1,450:
local appendix2 = canonical_name .. " numbers"
local appendix2 = canonical_name .. " numbers"
local appendix
local appendix
local title = appendix2
local title
if mw.title.new(appendix1, "Appendix"):getContent() then
appendix = appendix1
elseif mw.title.new(appendix2, "Appendix"):getContent() then
appendix = appendix2
end
 
if appendix then
title = "[[wikt:Appendix:" .. appendix .. "|" .. appendix2 .. "]]"
else
title = appendix2
end


local function format_cell(contents, class_name, colspan, bold)
local function format_cell(contents, class_name, colspan, bold)
Line 1,038: Line 1,504:
if cur_data.wplink then
if cur_data.wplink then
local footer =
local footer =
"[[w:" .. lang:getCode() .. ":Main Page|" .. lang:getCanonicalName() .. " Wikipedia]] article on " ..
"[[w:" .. lang:getCode() .. ":|" .. lang:getCanonicalName() .. " Wikipedia]] article on " ..
m_links.full_link{lang = lang, term = "w:" .. lang:getCode() .. ":" .. cur_data.wplink,
m_links.full_link{lang = lang, term = "w:" .. lang:getCode() .. ":" .. cur_data.wplink,
alt = export.format_number_for_display(cur_num)}
alt = number_system.format_key_for_display(cur_num)}
footer_display = '|- style="text-align: center;"\n' .. format_cell(footer, "footer-cell", has_outer_display and 5 or 3)
footer_display = '|- style="text-align: center;"\n' .. format_cell(footer, "footer-cell", has_outer_display and 5 or 3)
else
else
Line 1,050: Line 1,516:
" edit]</span>)</sup>"
" edit]</span>)</sup>"


return [=[{| class="floatright number-box" cellpadding="5" cellspacing="0" style="background: var(--wikt-palette-white, #ffffff); color: inherit; border: 1px #aaa solid; border-collapse: collapse; margin-top: .5em;" rules="all"
return [=[{| class="floatright number-box" cellpadding="5" cellspacing="0" style="background: var(--wikt-palette-white, #ffffff); color: inherit; border: 1px var(--border-color-base,#aaa) solid; border-collapse: collapse; margin-top: .5em;" rules="all"
|+ ''']=] .. title .. edit_link .. "'''\n" ..
|+ ''']=] .. title .. edit_link .. "'''\n" ..
upper_display .. '|- style="text-align: center;"\n' ..
upper_display .. '|- style="text-align: center;"\n' ..
Line 1,121: Line 1,587:
local optional2_term = args.opt2x; local optional2_alt = args.opt2xalt; local optional2_tr = args.opt2xtr
local optional2_term = args.opt2x; local optional2_alt = args.opt2xalt; local optional2_tr = args.opt2xtr


local subpage = mw.title.getCurrentTitle().subpageText
track(lang:getCode())
 
if sc then
track("sc")
end
 
if headlink then
track("headlink")
end
 
if wplink then
track("wplink")
end
 
if alt then
track("alt")
end
 
if cardinal_alt or ordinal_alt or adverbial_alt or multiplier_alt or distributive_alt or collective_alt or fractional_alt or optional1_alt or optional2_alt then
track("xalt")
end
 
local subpage = mw.loadData("Module:headword/data").pagename
local is_reconstructed = lang:hasType("reconstructed") or mw.title.getCurrentTitle().nsText == "Reconstruction"
local is_reconstructed = lang:hasType("reconstructed") or mw.title.getCurrentTitle().nsText == "Reconstruction"
Line 1,200: Line 1,688:
if wplink then
if wplink then
footer =
footer =
"[[w:" .. lang:getCode() .. ":Main Page|" .. lang:getCanonicalName() .. " Wikipedia]] article on " ..
"[[w:" .. lang:getCode() .. ":|" .. lang:getCanonicalName() .. " Wikipedia]] article on " ..
m_links.full_link{lang = lang, sc = sc, term = "w:" .. lang:getCode() .. ":" .. wplink, alt = alt, tr = tr}
m_links.full_link{lang = lang, sc = sc, term = "w:" .. lang:getCode() .. ":" .. wplink, alt = alt, tr = tr}
end
end