Module:number list: Difference between revisions

From Linguifex
Jump to navigation Jump to search
No edit summary
No edit summary
 
(33 intermediate revisions by 2 users not shown)
Line 1: Line 1:
local m_links = require("Module:links")
local m_str_utils = require("Module:string utilities")
local char = string.char
local concat = table.concat
local gsub = m_str_utils.gsub
local insert = table.insert
local list_to_set = require("Module:table").listToSet
local sort = table.sort
local split = m_str_utils.split
local u = m_str_utils.char
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local upper = string.upper
local export = {}
local export = {}
local decimal_strategy


local m_links = require("Module:links")
--[=[
 
Terminology:
 
Number = a bare number; a mathematical entity which has different form types (e.g. cardinal, ordinal)
Form type = a category of the forms that represent a number; examples are cardinal, ordinal, distributive, fractional
Form = a word or expression that represents a number in a given language
Tag = an identifier attached to a form that allows different logical subtypes of forms from the same form type to be
      identified; e.g. 'vuitanta-vuit<tag:Central>' vs. 'huitanta-huit<tag:Valencian>' to identify variants of
  Catalan cardinal number 88 for different dialectal standards; there can be multiple tags per form, e.g.
  'tair ar ddeg<tag:vigesimal><tag:feminine>' for the Welsh number 13 where there are both decimal/vigesimal and
  masculine/feminine variants of this number
Tag list = a list of tags in the order they are specified in the data, e.g. {"vigesimal", "feminine"} for the example
          above
Combined tag = the string representation of a tag list, using ||| to separate individual tags
]=]
 
local default_form_types = {
{key = "cardinal", display = "[[wikt:cardinal number|Cardinal]]"},
{key = "ordinal", display = "[[wikt:ordinal number|Ordinal]]"},
{key = "ordinal_abbr", display = "[[wikt:ordinal number|Ordinal]] [[wikt:abbreviation|abbreviation]]"},
{key = "adverbial", display = "[[wikt:adverbial number|Adverbial]]"},
{key = "multiplier", display = "[[wikt:multiplier|Multiplier]]"},
{key = "distributive", display = "[[wikt:distributive number|Distributive]]"},
{key = "collective", display = "[[wikt:collective number|Collective]]"},
{key = "fractional", display = "[[wikt:fractional|Fractional]]"},
}


local form_types = {
-- Keys in a `numbers` entry that aren't form types.
{key = "cardinal", display = "[[cardinal number|Cardinal]]"},
local non_form_types = {
{key = "ordinal", display = "[[ordinal number|Ordinal]]"},
numeral = true,
{key = "adverbial", display = "[[adverbial number|Adverbial]]"},
wplink = true,
{key = "multiplier", display = "[[multiplier|Multiplier]]"},
next = true,
{key = "distributive", display = "[[distributive number|Distributive]]"},
prev = true,
{key = "collective", display = "[[collective number|Collective]]"},
next_outer = true,
{key = "fractional", display = "[[fractional|Fractional]]"},
prev_outer = true,
upper = true,
lower = true,
}
}


local function index_of_number_type(t, type)
local function track(page)
for i, subtable in ipairs(t) do
require("Module:debug/track")("number list/" .. page)
if subtable.key == type then
return true
return i
end
 
--[=[
--
-- General set intersection
local function set_intersection(sets)
local intersection = {}
for key, _ in pairs(sets[1]) do
intersection[key] = true
end
for i = 2, #sets do
local this_set = sets[i]
for key, _ in pairs(intersection) do
if not this_set[key] then
-- See https://stackoverflow.com/questions/6167555/how-can-i-safely-iterate-a-lua-table-while-keys-are-being-removed
-- It is safe to modify or remove a key while iterating over the table.
intersection[key] = nil
end
end
end
return intersection
end
]=]
 
local function set_intersection(set1, set2)
local intersection = {}
for key, _ in pairs(set1) do
intersection[key] = true
end
for key, _ in pairs(intersection) do
if not set2[key] then
-- See https://stackoverflow.com/questions/6167555/how-can-i-safely-iterate-a-lua-table-while-keys-are-being-removed
-- It is safe to modify or remove a key while iterating over the table.
intersection[key] = nil
end
end
end
end
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
function export.get_data_module_name(langcode)
return "Module:number list/data/" .. langcode
end
-- Format a number (either a Lua number or a string) in fixed point without any decimal point or scientific notation.
-- `tostring()` doesn't work because it converts large numbers such as 1000000000000000 to "1e+15".
function export.format_fixed(number)
if type(number) == "string" then
return number
else
return ("%.0f"):format(number)
end
end
-- 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
-- `form` for the form, and `tr`, `tag`, `q`, `qq`, `g` or `link` for the modifiers. The `tag` field is a tag list
-- (see above).
function export.parse_form_and_modifiers(form_with_modifiers)
local retval = {}
local form
form = form_with_modifiers
while true do
local new_form, angle_bracketed = form:match("^(.-)(%b<>)$")
if not new_form then
break
end
local prefix, content = angle_bracketed:match "^<(%w+):(.+)>$"
if not prefix then
break
end
if prefix == "tag" then
if retval.tag then
insert(retval.tag, content)
else
retval.tag = {content}
end
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
error(("Duplicate modifier '%s' in data module form, already saw value '%s': %s"):format(prefix,
retval[prefix], form_with_modifiers))
else
retval[prefix] = content
end
else
error(("Unrecognized modifier '%s' in data module form: %s"):format(prefix, form_with_modifiers))
end
form = new_form
end
retval.form = form
return retval
end
-- Find the `numbers` object for a given number (which should be in string representation).
function export.lookup_data(m_data, numstr)
-- Don't try to convert very large numbers to Lua numbers because they may overflow.
-- Powers of 10 >= 10^22 cannot be represented exactly as a Lua number.
return m_data.numbers[numstr] or #numstr < 22 and m_data.numbers[tonumber(numstr)] or nil
end
-- Return true if a < b, where either may be a Lua number or the string representation of a number.
function export.numbers_less_than(a, b)
a, b = export.format_fixed(a), export.format_fixed(b)
local alen = #a
local blen = #b
if alen < blen then
return true
end
if alen > blen then
return false
end
return a < b
end
-- Return true if a > b, where either may be a Lua number or the string representation of a number.
function export.numbers_greater_than(a, b)
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
-- Given a number form, convert it to its independent (un-affixed) form. This only makes sense for certain languages
-- where there is a difference between independent and affixed forms of numerals. Currently the only such language
-- is Swahili, where e.g. the cardinal number form for 3 is affixed [[-tatu]], independent [[tatu]], and the ordinal
-- number form is [[-a tatu]], independent [[tatu]]. We rely on a set of Lua pattern substitutions to convert from
-- affixed to independent form.
--
-- FIXME: This needs major rethinking in a way that isn't specific to Swahili.
local function maybe_unaffix(m_data, form)
if not m_data.unaffix then
return form
end
for _, entry in ipairs(m_data.unaffix) do
local from, to = unpack(entry)
form = gsub(form, from, to)
end
return form
end
-- Convert the given number form (taken from the data for `lang`, after parsing the form for modifiers and stripping
-- the modifiers) to the stripped-text version of the form. The form may have links and/or accent/length marks that need
-- to be stripped.
local function form_to_stripped_form(form, lang)
return lang:stripDiacritics(m_links.remove_links(form))
end
-- Return true if the given number form object (taken from the data for `lang`, after parsing the form for modifiers)
-- matches `pagename`. If there is a <link:...> modifier, we check against it. Otherwise, we check against the form
-- itself. In this case, the form may have links and/or accent/length marks that need to be stripped, and we may need
-- to convert the form to its independent (un-affixed) form, if there is a difference between independent and affixed
-- forms (as in Swahili).
local function form_equals_pagename(formobj, pagename, m_data, lang)
if formobj.link == pagename then
return true
end
local stripped_form = form_to_stripped_form(formobj.form, lang)
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
-- 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).
local function get_next_and_prev_keys(m_data, numstr, strategy, lookup_data)
local numdata = lookup_data(numstr)
if not numdata then
return nil, nil
end
local nextnum = numdata.next
local prevnum = numdata.prev
if not nextnum or not prevnum then
-- Find the next/previous numbers by sorting all the keys and locating the number in question among them.
local sorted_list = {}
local seen = {}
local index = 1
for key, _ in pairs(m_data.numbers) do
local normalized_key = strategy.normalize_data_key(key)
if not seen[normalized_key] then
seen[normalized_key] = true
sorted_list[index] = normalized_key
index = index + 1
end
end
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
-- matter to search linearly.
for i, key in ipairs(sorted_list) do
if key == numstr then
nextnum = nextnum or sorted_list[i + 1]
prevnum = prevnum or sorted_list[i - 1]
break
end
end
end
if nextnum then
nextnum = strategy.normalize_data_key(nextnum)
end
if prevnum then
prevnum = strategy.normalize_data_key(prevnum)
end
return nextnum, prevnum
end
-- Find the "description objects" (a two-element list {NUMBER, TYPE}, where NUMBER is either a Lua number or a string,
-- depending on how it appears in the underlying data) that matches `pagename` and (if given) `matching_type`.
-- Return a list of such objects.
local function lookup_number_by_form(lang, m_data, pagename, matching_type)
local retval = {}
local function check_form(form, num, typ)
local formobj = export.parse_form_and_modifiers(form)
if form_equals_pagename(formobj, pagename, m_data, lang) and (not matching_type or typ == matching_type) then
-- It's possible the same pagename occurs multiply for a given type and number, e.g. with different length
-- or accent marks. The calling code is OK with multiple entries for a given number (which can also occur
-- with different types, e.g. the ordinal and fractional forms for a given number are the same), but will
-- throw an error if different numbers are seen.
insert(retval, {num, typ})
end
end
for num, numdata in pairs(m_data.numbers) do
for numtype, forms in pairs(numdata) do
if non_form_types[numtype] then
-- do nothing
elseif type(forms) == "table" then
for _, form in ipairs(forms) do
check_form(form, num, numtype)
end
else
check_form(forms, num, numtype)
end
end
end
return retval
end
end


Line 25: Line 418:
-- the numeral type that the form should appear before or after.
-- the numeral type that the form should appear before or after.
-- The transformations are applied in order.
-- The transformations are applied in order.
local function add_form_types(additional_types)
local function add_form_types(form_types, additional_types)
local types = require "Module:table".deepcopy(form_types)
local types = require("Module:table").deepCopy(form_types)
for _, type in ipairs(additional_types) do
for _, additional_type in ipairs(additional_types) do
type = require "Module:table".shallowcopy(type)
if not (additional_type.before or additional_type.after) then
local i
insert(types, additional_type)
if type.before or type.after then
else
i = index_of_number_type(types, type.before or type.after)
if additional_type.before and additional_type.after then
end
error("The form type '" .. additional_type.key .. "' is specifying both before and after, which is not allowed")
-- For now, simply log an error message
end
-- if the "before" or "after" number type was not found,
 
-- and insert the number type at the end.
local anchor, index = additional_type.before or additional_type.after
if i then
 
if type.before then
for i, another_type in ipairs(types) do
table.insert(types, i - 1, type)
if another_type.key == anchor then
else
index = i
table.insert(types, i + 1, type)
break
end
end
 
if index and additional_type.after then
index = index + 1
end
end
else
 
table.insert(types, type)
additional_type = require("Module:table").shallowCopy(additional_type)
if type.before or type.after then
additional_type.before, additional_type.after = nil, nil
if not index then
mw.log("Number type "
mw.log("Number type "
.. (type.before or type.after)
.. (additional_type.before or additional_type.after)
.. " was not found.")
.. " was not found.")
insert(types, additional_type)
else
insert(types, index, additional_type)
end
end
end
end
type.before, type.after = nil, nil
end
end
return types
return types
end
end


function export.get_number_types(language_code)
-- Return all form types for the language in question, in order.
local m_data = require("Module:number list/data/" .. language_code)
function export.get_number_types(m_data)
local final_form_types = form_types
local form_types = default_form_types
if m_data.additional_number_types then
if m_data.additional_number_types then
final_form_types = add_form_types(m_data.additional_number_types)
return add_form_types(form_types, m_data.additional_number_types)
else
return form_types
end
end
return final_form_types
end
end


-- Convert a number type object (an object with `display` and `key` fields) to its displayed form.
function export.display_number_type(number_type)
function export.display_number_type(number_type)
if number_type.display then
if number_type.display then
return number_type.display
return number_type.display
else
else
return (number_type.key:gsub("^.", string.upper):gsub("_", " "))
return (number_type.key:gsub("^.", upper):gsub("_", " "))
end
end
end
end


local a = ("a"):byte()
-- Group digits with a separator, such as a comma or a period. See [[w:Digit grouping]].
local function multiple_num_links(terms)
local function add_separator(numstr, separator, group, start)
local links = {}
start = start or group
for i, term in ipairs(terms) do
if start >= #numstr then
links[i] = m_links.language_link{term = term, alt = "[" .. string.char(a + i - 1) .. "]", tr = "-"}
return numstr
end
end
return "<sup>" .. table.concat(links, ", ") .. "</sup>"
end


function map(func, array)
local parts = { numstr:sub(-start) }
local new_array = {}
for i = start + 1, #numstr, group do
for i,v in ipairs(array) do
insert(parts, 1, numstr:sub(-(i + group - 1), -i))
new_array[i] = func(v)
end
end
return new_array
end


local function unsuffix(term)
return concat(parts, separator)
if type(term) == "table" then
return map(unsuffix, term)
end
if term:find("^-a ") ~= nil then
return term:sub(4)
end
if term:sub(1, 1) == "-" then
return term:sub(2)
end
return term
end
end


-- See [[w:Digit grouping]].
function export.add_thousands_separator(numstr, separator)
local function add_separator(number, separator, group, start)
if #numstr < 4 then -- < 1000
number = tostring(number)
return numstr
start = start or group
if start >= #number then
return number
end
local parts = { number:sub(-start) }
for i = start + 1, #number, group do
table.insert(parts, 1, number:sub(-(i + group - 1), -i))
end
end
return add_separator(numstr, separator or ",", 3)
return table.concat(parts, separator)
end
end


local function add_thousands_separator(number, separator)
local function add_Indic_separator(numstr, separator)
return add_separator(number, separator, 3)
return add_separator(numstr, separator, 2, 3)
end
end


local function add_Indic_separator(number, separator)
-- Convert a number (represented as a string) to non-Arabic form based on the specs in `numeral_config`.
return add_separator(number, separator, 2, 3)
-- This is used, for example, to display the Hindu, Eastern Arabic or Roman form of a number along with the standard
end
-- Arabic form. Most of the code below assumes that the non-Arabic numerals are decimal, and the digits map one-to-one
 
-- with Arabic numerals. If this is not the case (e.g. for Roman numerals), a special module function is called to do
function export.generate_decimal_numeral(numeral_config, number)
-- the conversion.
if type(number) ~= "number" then
function export.generate_non_arabic_numeral(numeral_config, numstr)
return nil
-- `numstr` is a number represented as a string. See comment near top of show_box().
end
if numeral_config.module and numeral_config.func then
if numeral_config.module and numeral_config.func then
return require("Module:" .. numeral_config.module)[numeral_config.func](number)
return require("Module:" .. numeral_config.module)[numeral_config.func](numstr)
end
end
 
local thousands_separator, Indic_separator, zero_codepoint =
local thousands_separator, Indic_separator, zero_codepoint =
numeral_config.thousands_separator,
numeral_config.thousands_separator,
numeral_config.Indic_separator,
numeral_config.Indic_separator,
numeral_config.zero_codepoint
numeral_config.zero_codepoint
 
if not zero_codepoint then
if not zero_codepoint then
return nil
return nil
end
end
 
local number_string = tostring(number)
if thousands_separator then
if thousands_separator then
number_string = add_thousands_separator(number_string, thousands_separator)
numstr = export.add_thousands_separator(numstr, thousands_separator)
elseif Indic_separator then
elseif Indic_separator then
number_string = add_Indic_separator(number_string, Indic_separator)
numstr = add_Indic_separator(numstr, Indic_separator)
end
end
 
return number_string:gsub("[0-9]", function (digit)
return numstr:gsub("%d", function (digit)
return mw.ustring.char(zero_codepoint + tonumber(digit))
return u(zero_codepoint + tonumber(digit))
end)
end)
end
end




local function remove_duplicate_entry_names(lang, terms)
-- Format a number (either a Lua number or a string) for display. Sufficiently small numbers are displayed in fixed
local entries = require "Module:fun".map(
-- point with thousands separators. Larger numbers are displayed in both fixed point and scientific notation using
function(term) return lang:makeEntryName(term) end,
-- superscripts, and sufficiently large numbers are displayed only in scientific notation.
terms)
function export.format_number_for_display(number)
local entry_set = {}
local MAX_NUM_DIGITS_FOR_FIXED_ONLY = 6
return require "Module:fun".filter(
local MIN_NUM_DIGITS_FOR_SCIENTIFIC_ONLY = 13
function(entry)
local numstr = export.format_fixed(number)
local already_seen = entry_set[entry]
local fixed = export.add_thousands_separator(numstr)
entry_set[entry] = true
if #numstr <= MAX_NUM_DIGITS_FOR_FIXED_ONLY then
return not already_seen
return fixed
end,
end
entries)
local kstr = numstr:match("^([0-9]*[1-9])0*$")
if not kstr then
error("Internal error: Unable to match number '" .. numstr .. "'")
end
local exponent = ("10<sup>%s</sup>"):format(#numstr - 1)
local mantissa
if kstr == "1" then
mantissa = ""
elseif #kstr == 1 then
mantissa = kstr .. " × "
else
mantissa = kstr:gsub("^([0-9])", "%1.") .. " × "
end
local scientific = mantissa .. exponent
if #numstr >= MIN_NUM_DIGITS_FOR_SCIENTIFIC_ONLY then
return scientific
else
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
-- Map a list of tags to a single string that is equivalent. We need to do this because we can't easily put lists in the
-- keys of tables.
local function tag_list_to_combined_tag(tag_list)
return concat(tag_list, "|||")
end
-- Given a list of forms with attached inline modifiers (e.g. 'huitanta-huit<tag:Valencian>' or
-- 'tair ar ddeg<tag:vigesimal><tag:feminine>'), parse the forms into form objects (the return value of
-- parse_form_and_modifiers()) and group by the tag. Three values are returned:
-- `seen_forms`, `forms_by_tag`, `seen_tags` where:
-- (1) `seen_forms` is the list of parsed form objects;
-- (2) `forms_by_tag` is a table grouping the form objects by combined tag, where the key is the tag and the value is
--      a list of the form objects seen with that tag (forms without tag are grouped under the empty-string tag);
-- (3) `seen_tags` is a list of the combined tags encountered, in the order they were encountered;
-- (4) `combined_tags_to_tag_lists` is a map from combined tags to the corresponding tag lists.
function export.group_numeral_forms_by_tag(forms)
local seen_forms = {}
local forms_by_tag = {}
local seen_tags = {}
local combined_tags_to_tag_lists = {}
for _, form in ipairs(forms) do
local formobj = export.parse_form_and_modifiers(form)
insert(seen_forms, formobj)
local combined_tag = formobj.tag and tag_list_to_combined_tag(formobj.tag) or ""
if not forms_by_tag[combined_tag] then
insert(seen_tags, combined_tag)
forms_by_tag[combined_tag] = {}
combined_tags_to_tag_lists[combined_tag] = formobj.tag or {}
end
insert(forms_by_tag[combined_tag], formobj)
end
return seen_forms, forms_by_tag, seen_tags, combined_tags_to_tag_lists
end
-- Given a form object (as returned by parse_form_and_modifiers()), format as appropriate for the current language.
function export.format_formobj(formobj, m_data, lang)
local left_q = formobj.q and require("Module:qualifier").format_qualifier(formobj.q) .. " " 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 alt = formobj.alt
if not alt and term ~= formobj.form then
alt = formobj.form
end
return left_q .. m_links.full_link{
lang = lang, term = term, alt = alt, tr = formobj.tr, id = formobj.id,
} .. right_q
end
-- Implementation of {{number box}}.
function export.show_box(frame)
function export.show_box(frame)
local full_link = m_links.full_link
local full_link = m_links.full_link
 
local params = {
local params = {
[1] = {required = true},
[1] = {required = true, type = "language", default = "und"},
[2] = {required = true},
[2] = true,
[3] = {},
["pagename"] = true,
[4] = {},
["type"] = true,
["type"] = {},
}
}
 
local args = require("Module:parameters").process(frame:getParent().args, params)
local parent_args = frame:getParent().args
if parent_args.pagename then
local cur_num = args[1] or 1
track("show-box-pagename")
end
local args = require("Module:parameters").process(parent_args, params, nil, "number list", "show_box")
 
local lang = args[1]
local langcode = lang:getCode()
 
-- Get the data from the data module. Some modules (e.g. currently [[Module:number list/data/ka]]) have to be
-- loaded with require() because the exported numbers table has a metatable.
local module_name = export.get_data_module_name(langcode)
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.loadData("Module:headword/data").pagename
local cur_type = args.type
local cur_type = args.type
if not (type(cur_num) == "number" or cur_num:find "^%d+$") then
 
error("Extraneous characters in parameter 2: should be decimal number (integer).")
-- We represent all numbers as strings in this function to deal with the limited precision inherent in Lua numbers.
-- These large numbers do occur, such as 100 trillion ([[རབ་བཀྲམ་ཆེན་པོ]]), 1 sextillion, etc. Lua represents all
-- numbers as 64-bit floats, meaning that some numbers above 2^53 cannot be represented exactly. The first power of
-- 10 that cannot be represented exactly is 10^22 (ten sextillion in short scale, ten thousand trillion in long
-- scale), but the first power of ten whose neighboring numbers cannot be represented exactly is 10^16 (ten
-- quadrillion or ten thousand billion). Ideally we would use a big integer library of some kind, but unfortunately
-- Wiktionary does not seem to have any such library installed. MediaWiki docs make mention of bcmath, but
-- mw.bcmath.new() throws an error.
--
-- In module data, we allow numbers to be indexed as Lua numbers or as strings. See lookup_data() above.
local cur_num = args[2] or langcode == "und" and mw.title.getCurrentTitle().nsText == "Template" and "2" or nil
 
-- If a current number wasn't specified, find it by looking through the data for the current language and matching
-- forms against the pagename.
if not cur_num then
local nums_and_types = lookup_number_by_form(lang, m_data, pagename, cur_type)
if #nums_and_types == 0 then
error("The current page name '" .. pagename .. "' does not match the spelling of any known number in [[" ..
module_name .. "]]. Check the data module or the spelling of the page.")
end
for _, num_and_type in ipairs(nums_and_types) do
local num = num_and_type[1]
num = number_system.normalize_data_key(num)
if cur_num and num ~= cur_num then
local errparts = {}
for _, num_and_type in ipairs(nums_and_types) do
local num, typ = unpack(num_and_type)
num = number_system.normalize_data_key(num)
insert(errparts, ("%s (%s)"):format(num, typ))
end
error("The current page name '" .. pagename .. "' matches the spelling of multiple numbers in [[" ..
module_name .. "]]: " .. concat(errparts, ",") .. ". Please specify the number explicitly.")
else
cur_num = num
end
end
end
 
local function candidate_matches_pagename(candidate_num)
local candidate_data = number_system.lookup_data(m_data, candidate_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
end
local alt_pagename = args[2] or false
 
local remove_suffix = args[3] or false
local cur_num_candidates = number_system.normalize_input_candidates and
number_system.normalize_input_candidates(cur_num) or
-- Get the data from the data module.
{number_system.normalize_input(cur_num)}
-- [[Module:number list/data/en]] has to be loaded with require because its
cur_num = cur_num_candidates[1]
-- exported numbers table has a metatable.
if #cur_num_candidates > 1 then
local module_name = "Module:number list/data/" .. lang:getCode()
for _, candidate in ipairs(cur_num_candidates) do
local m_data = require(module_name)
if candidate_matches_pagename(candidate) then
cur_num = candidate
-- Numbers that can't be represented exactly as a Lua number
break
-- must be stored as a string. The first power of 10 that cannot be
end
-- represented exactly is 10^22 (ten sextillion in short scale,
end
-- ten thousand trillion in long scale), but the first power of ten whose
end
-- neighboring numbers cannot be represented exactly
 
-- is 10^16 (ten quadrillion or ten thousand billion).
-- Wrapper around `export.lookup_data` that may throw an error if the number can't be found (specifically if
-- The number data must be looked up using the string, but after that
-- param_for_error is given).
-- the number string must be converted to a number, because some code
local function lookup_data(numstr, param_for_error)
-- after this point requires a number. This might cause bugs!
numstr = number_system.normalize_data_key(numstr)
-- Ideally we would use a big integer library of some kind.
local retval = number_system.lookup_data(m_data, numstr)
local cur_data = m_data.numbers[cur_num] or m_data.numbers[tonumber(cur_num)]
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 '
.. "[[%s]]; please fix the module."):format(param_for_error, numstr, cur_num, module_name))
end
return retval
end
 
local cur_data = lookup_data(cur_num)
if not cur_data then
if not cur_data then
error('The number "' .. cur_num .. '" is not found in the "numbers" table in [[' .. module_name .. ']].')
error('The number "' .. cur_num .. '" is not found in the "numbers" table in [[' .. module_name .. "]].")
end
end
-- Save original cur_num if it is a string, for use below.
 
orig_cur_num = cur_num
local formatted_forms = {}
cur_num = tonumber(cur_num)
 
-- Go over each number and make links
local forms = {}
local full_pagename = (mw.title.getCurrentTitle().nsText=="Reconstruction" and "*" or "") .. mw.title.getCurrentTitle().subpageText
if alt_pagename then full_pagename = alt_pagename end
if cur_type and not cur_data[cur_type] then
if cur_type and not cur_data[cur_type] then
error("The numeral type " .. cur_type .. " for " .. orig_cur_num
error("The numeral type " .. cur_type .. " for " .. cur_num .. " is not found in [[" .. module_name .. "]].")
.. " is not found in [[" .. module_name .. "]].")
end
end
-- See above for the definition of "combined tag" and "tag list". The combined tag is just the concatenation of the
-- tag list with ||| between the tags.
local cur_tag_list, cur_combined_tag
local form_types = export.get_number_types(m_data)
for _, form_type in ipairs(export.get_number_types(lang:getCode())) do
-- LONG COMMENT EXPLAINING TAG HANDLING:
--
-- For each form type (see `form_types` at top of file), group the entries for that form type by tag and figure out
-- what the current form type and tag is, i.e. the form type and tag for the form matching the pagename. Tags are
-- e.g. as in 'vuitanta-vuit<tag:Central>' or 'huitanta-huit<tag:Valencian>' for Catalan and allow different
-- logical sets of numbers for the same form type to be identified. There can potentially be multiple tags per
-- form, e.g. 'tair ar ddeg<tag:vigesimal><tag:feminine>' for the Welsh number 13 where there are both decimal/
-- vigesimal and masculine/feminine variants of this number.
--
-- We need to do two passes over all form types. In the first pass, for each form type we parse all the forms,
-- group them by tag, and store the results in a per-form-type table. In the second pass, we then format all forms
-- for all form types. The reason for doing two passes is because we need to know the current tag in order to
-- display a form type correctly (because we display the forms for the current tag before the forms for any other
-- tags), but we won't know the current tag until we have done a pass over all form types and forms of those form
-- types in order to determine which one matches the pagename.
--
-- We use the current tag in two ways:
-- 1. When displaying all the forms for a given number, we group both by form type and tag, and display the forms
--    for a given form type/tag combination on a single line. For a given form type, we display the forms for each
--    tag in the order the tags were specified in the data, except that the forms for the current tag are placed
--    before all others (so e.g. for Catalan, if the current tag is "Valencian", we list the Valencian form(s)
--    first even if the Central form(s) are listed first in the data file).
-- 2. When displaying links to adjacent numbers in display_adjacent_number_links(), if there aren't form(s) for the
--    current type, we don't display any links; but if there are mutiple tagged forms for the current type, we only
--    display links for the forms for the current tag if there are any such forms, otherwise we display links for
--    all forms of all tags.
--
-- In the presence of multiple tags, things get a bit more complicated:
-- 1. When displaying links to adjacent numbers, say the current tag is vigesimal+feminine, we want to prefer an
--    adjacent-number form that's both vigesimal and feminine, but otherwise we prefer one that's vigesimal or
--    feminine over one that's neither. Say the current tag is just vigesimal; we of course prefer an
--    adjacent-number form that's just vigesimal, but otherwise we prefer a tag that's vigesimal + either masculine
--    or feminine to a tag that's not vigesimal. So it seems we want the form(s) that have the maximum intersection
--    of tags, and if there are two different tag lists with the same number of intersecting tags (e.g. the current
--    tag is vigesimal+feminine and we have a choice of decimal+feminine or just vigesimal), we should prefer the
--    form that has fewer non-matching tags, hence we prefer the just-vigesimal form.
-- 2. By the same logic, when displaying all the forms for a given number, we should order by the size of the
--    intersection of the tag list in question with the current tag list, then inversely by the size of the tag list
--    (so we prefer tag lists with fewer non-matching tags), then by the order of the tag lists in the data file.
 
local forms_by_tag_per_form_type = {}
local seen_tags_per_form_type = {}
local combined_tags_to_tag_lists_per_form_type = {}
 
for _, form_type in ipairs(form_types) do
local numeral = cur_data[form_type.key]
local numeral = cur_data[form_type.key]
if numeral then
if numeral then
local form = {}
local seen_forms, forms_by_tag, seen_tags, combined_tags_to_tag_lists = export.group_numeral_forms_by_tag(
local numerals
type(numeral) == "table" and numeral or {numeral}
if type(numeral) == "string" then
)
numerals = {numeral}
forms_by_tag_per_form_type[form_type] = forms_by_tag
elseif type(numeral) == "table" then
seen_tags_per_form_type[form_type] = seen_tags
numerals = numeral
combined_tags_to_tag_lists_per_form_type[form_type] = combined_tags_to_tag_lists
for _, formobj in ipairs(seen_forms) do
if not cur_tag_list and form_equals_pagename(formobj, pagename, m_data, lang) then
cur_tag_list = formobj.tag or {}
cur_combined_tag = tag_list_to_combined_tag(cur_tag_list)
cur_type = cur_type or form_type.key
end
end
end
end
 
-- Error if we couldn't locate the pagename among the forms for the current number. This only happens if the
-- number if given explicitly in 2=.
 
if not cur_type and mw.title.getCurrentTitle().nsText ~= "Template" then
error("The current page name '" .. pagename .. "' does not match any of the numbers listed in [[" ..
module_name .. "]] for " .. cur_num .. ". Check the data module or the spelling of the page.")
end
 
-- Now, format all the forms for all form types for the current number.
 
local function sort_combined_tags(combined_tags, seen_tags, combined_tags_to_tag_lists)
-- cur_tag_list should normally never be nil, but can be so in template space
local cur_tag_set = list_to_set(cur_tag_list or {})
local tags_to_order = {}
for i, tag in ipairs(seen_tags) do
tags_to_order[tag] = i
end
local function compare_tags(tag1, tag2)
-- See long comment above.
-- First compare by number of tags in common with the current tag list.
local tag_list1 = combined_tags_to_tag_lists[tag1]
local tag_list2 = combined_tags_to_tag_lists[tag2]
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 n_common1, n_common2 = set_size(common1), set_size(common2)
if n_common1 ~= n_common2 then
return n_common1 > n_common2 -- larger overlap with current tag list first
end
-- When overlap ties, shorter tag lists first (untagged default before explicit <tag:...> rows).
if #tag_list1 ~= #tag_list2 then
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
for _, numeral in ipairs(numerals) do
-- tag, put it first, and if somehow we encounter a tag that's not in the original ordering, put it last.
-- If this number is the current page, then store the key for later use
local index1 = tag1 == cur_combined_tag and 0 or tags_to_order[tag1] or #seen_tags + 1
if not cur_type and lang:makeEntryName(numeral) == full_pagename then
local index2 = tag2 == cur_combined_tag and 0 or tags_to_order[tag2] or #seen_tags + 1
cur_type = form_type.key
return index1 < index2
end
sort(combined_tags, compare_tags)
end
 
for _, form_type in ipairs(form_types) do
local forms_by_tag = forms_by_tag_per_form_type[form_type]
local seen_tags = seen_tags_per_form_type[form_type]
local combined_tags_to_tag_lists = combined_tags_to_tag_lists_per_form_type[form_type]
if forms_by_tag then
local function insert_forms_by_tag(tag)
local formatted_tag_forms = {}
 
local pagename_among_forms = false
for _, formobj in ipairs(forms_by_tag[tag]) do
insert(formatted_tag_forms, export.format_formobj(formobj, m_data, lang))
if form_equals_pagename(formobj, pagename, m_data, lang) then
pagename_among_forms = true
end
end
 
if tag ~= "" then
local tag_list = combined_tags_to_tag_lists[tag]
tag = concat(tag_list, " / ")
end
local displayed_number_type = export.display_number_type(form_type) .. (tag == "" and "" or (" (%s)"):format(tag))
if pagename_among_forms then
displayed_number_type = "'''" .. displayed_number_type .. "'''"
end
end
 
table.insert(form, full_link({lang = lang, term = remove_suffix and unsuffix(numeral) or numeral, alt = numeral }))
insert(formatted_forms, " &nbsp;&nbsp;&nbsp; ''" .. displayed_number_type .. "'': " ..
concat(formatted_tag_forms, ", "))
end
end
 
local displayed_number_type = export.display_number_type(form_type)
sort_combined_tags(seen_tags, seen_tags, combined_tags_to_tag_lists)
if form_type.key == cur_type then
for _, tag in ipairs(seen_tags) do
displayed_number_type = "'''" .. displayed_number_type .. "'''"
insert_forms_by_tag(tag)
end
end
table.insert(forms, " &nbsp;&nbsp;&nbsp; ''" .. displayed_number_type .. "'': " .. table.concat(form, ", "))
end
end
end
end
 
if not cur_type and mw.title.getCurrentTitle().nsText ~= "Template" then
error("The current page name does not match any of the numbers listed in [[" .. module_name .. "]] for " .. cur_num .. ". Check the data module or the spelling of the page.")
end
-- Current number in header
-- Current number in header
-- Prevent large numbers, such as 100 trillion ([[རབ་བཀྲམ་ཆེན་པོ]]) from being
local cur_display = number_system.format_key_for_display(cur_num)
-- displayed in scientific notation (1+e14).
 
local cur_display = ("%i"):format(cur_num)
---[[
if cur_num >= 1000 then
cur_display = add_thousands_separator(cur_display, ",")
end
--]]
local numeral
local numeral
if m_data.numeral_config then
if m_data.numeral_config then
numeral = export.generate_decimal_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 = tostring(cur_data["numeral"])
numeral = number_system.normalize_data_key(cur_data["numeral"])
end
end
 
if numeral then
if numeral then
cur_display = full_link({lang = lang, alt = numeral, tr = "-"}) .. "<br/><span style=\"font-size: smaller;\">" .. cur_display .. "</span>"
cur_display = full_link{lang = lang, alt = numeral, tr = "-"} .. "<br/><span style=\"font-size: smaller;\">" .. cur_display .. "</span>"
end
end
 
-- Link to previous number
--------------------- Determine next/prev, next/prev outer, and upper/lower numbers. ----------------------
local prev_data = m_data.numbers[cur_num - 1]
 
local prev_display = ""
-- We have three series of numbers to determine:
--
-- Current format:
-- 1. The next/previous numbers, which are always those in the sorted series of available numbers unless overridden
--   by `next`/`prev` specs in an individual number.
-- 2. The next/previous outer numbers, which are displayed to the outside of the next/previous numbers. These can
--    be overridden for an individual number using `next_outer`/`prev_outer`. Otherwise, we try according to an
--    algorithm described below in the code for computing the outer numbers.
-- 3. The upper/lower numbers, which are displayed above or below the central number box. These can be overridden
--    for an individual number using `upper`/`lower`. These are always 10x greater or less than the number in
--    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, number_system, lookup_data)
local next_data = next_num and lookup_data(next_num, "next")
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 =
number_system.derive_related_numbers(cur_num, cur_data, next_num, prev_num, lookup_data)
 
-- For a number `num` (an "adjacent" number to the current number, i.e. either next, previous, next/previous outer,
-- or upper/lower) with corresponding entry data `num_data`, display link(s) to the form(s) for this number that
-- are associated with the current type and tag. If there is a single form to be linked to, the form is linked
-- using the number itself as the display text; otherwise, the multiple forms are linked with superscripted [a],
-- [b], etc. and the number it displayed adjacent to the links. In either case, beside the number there may be an
-- arrow. If `arrow` == "rarrow", the format is like this:
-- if multiple entries:
-- <numeral> → <sup>[a], [b], ...</sup>
-- else
-- <numeral> →
-- If `arrow` == "larrow", the format is like this:
-- if multiple entries:
-- if multiple entries:
-- <sup>[a], [b], ...</sup> ← <numeral>
-- <sup>[a], [b], ...</sup> ← <numeral>
-- else
-- else
-- ← <numeral>
-- ← <numeral>
local prev_num = prev_data and prev_data[cur_type]
-- Otherwise, the format is like this:
if prev_num then
-- if multiple entries:
local entries
-- <numeral><sup>[a], [b], ...</sup>
if type(prev_num) == "table" then
-- else
entries = remove_duplicate_entry_names(lang, prev_num)
-- <numeral>
--
-- Returns nil if `num_data` is nil or there is no entry in `num_data` for the current number type.
--
-- For the handling of tags in this function, see the "LONG COMMENT EXPLAINING TAG HANDLING" above.
local function display_adjacent_number_links(num, num_data, arrow)
if not num_data then
return nil
end
local forms = num_data[cur_type]
if not forms then
return nil
elseif type(forms) ~= "table" then
forms = {forms}
end
 
local seen_forms, forms_by_tag = export.group_numeral_forms_by_tag(forms)
 
-- FIXME: `cur_tag` is not defined. This seems to have been missed when multiple tag handling was added in [[Special:Diff/68978046]].
local forms_to_display
if cur_tag and forms_by_tag[cur_tag] then
forms_to_display = forms_by_tag[cur_tag]
else
else
entries = { prev_num }
forms_to_display = seen_forms
end
end
 
mw.logObject(prev_num, 'prev_num')
for i, form_to_display in ipairs(forms_to_display) do
mw.logObject(entries, 'entries')
forms_to_display[i] = form_to_display.link or maybe_unaffix(m_data,
form_to_stripped_form(form_to_display.form, lang))
if #entries > 1 then
end
prev_display = multiple_num_links(remove_suffix and unsuffix(entries) or entries, lang)
 
.. "&nbsp;←&nbsp;&nbsp;" .. (cur_num - 1)
local seen_pagenames = {}
else
local pagenames_to_display = {}
prev_display = m_links.language_link {
for _, form in ipairs(forms_to_display) do
lang = lang,
if not seen_pagenames[form] then
term = remove_suffix and unsuffix(entries[1]) or entries[1],
insert(pagenames_to_display, form)
alt = "&nbsp;←&nbsp;&nbsp;" .. (cur_num - 1),
seen_pagenames[form] = true
tr = "-",
end
}
end
end
end
 
if #pagenames_to_display == 0 then
-- Link to next number
return nil
local next_data = m_data.numbers[cur_num + 1]
local next_display = ""
-- Current format:
-- if multiple entries:
-- <numeral> → <sup>[a], [b], ...</sup>
-- else
-- <numeral> →
local next_num = next_data and next_data[cur_type]
if next_num then
local entries
if type(next_num) == "table" then
entries = remove_duplicate_entry_names(lang, next_num)
else
entries = { next_num }
end
end
 
if #entries > 1 then
num = number_system.format_key_for_display(num)
next_display = (cur_num + 1) .. "&nbsp;&nbsp;→&nbsp;"
local num_arrow =
.. multiple_num_links(remove_suffix and unsuffix(entries) or entries, lang)
arrow == "rarrow" and num .. "&nbsp;&nbsp;→&nbsp;" or
arrow == "larrow" and "&nbsp;←&nbsp;&nbsp;" .. num or
num
if #pagenames_to_display > 1 then
local a = ("a"):byte()
local links = {}
for i, term in ipairs(pagenames_to_display) do
links[i] = m_links.language_link{lang = lang, term = term, alt = "[" .. char(a + i - 1) .. "]"}
end
links = "<sup>" .. concat(links, ", ") .. "</sup>"
return arrow == "larrow" and links .. num_arrow or num_arrow .. links
else
else
next_display = m_links.language_link {
return m_links.language_link {
lang = lang,
lang = lang,
term = remove_suffix and unsuffix(entries[1]) or entries[1],
term = pagenames_to_display[1],
alt = (cur_num + 1) .. "&nbsp;&nbsp;→&nbsp;",
alt = num_arrow,
tr = "-",
}
}
end
end
end
end
 
-- Link to number times ten and divided by ten
-- Display links to previous/next numbers
-- Show this only if the number is a power of ten times a number 1-9 (that is, of the form x000...)
local prev_display = display_adjacent_number_links(prev_num, prev_data, "larrow") or ""
local up_display
local next_display = display_adjacent_number_links(next_num, next_data, "rarrow") or ""
local down_display
 
-- Display links to previous/next outer numbers
-- This test *could* be done numerically, but this is nice and simple and it works.
local prev_outer_display = display_adjacent_number_links(prev_outer_num, prev_outer_data, "larrow")
if tostring(cur_num):find("^[1-9]0*$") then
local next_outer_display = display_adjacent_number_links(next_outer_num, next_outer_data, "rarrow")
up_num = cur_num * 10
 
local up_data = m_data.numbers[up_num]
-- Display links to upper/lower numbers
local upper_display = display_adjacent_number_links(upper_num, upper_data)
if up_data and up_data[cur_type] then
local lower_display = display_adjacent_number_links(lower_num, lower_data)
if type(up_data[cur_type]) == "table" then
 
up_display = up_num .. multiple_num_links(remove_suffix and unsuffix(up_data[cur_type]) or up_data[cur_type], lang)
local canonical_name = lang:getCanonicalName()
else
local title = canonical_name .. " numerals"
up_display = full_link({lang = lang, term = remove_suffix and unsuffix(up_data[cur_type]) or up_data[cur_type], alt = up_num, tr = "-"})
 
end
local function format_cell(contents, class_name, colspan, bold)
end
class_name = class_name and (" " .. class_name) or ""
colspan = colspan and ('colspan="%s" '):format(colspan) or ""
-- Only divide by 10 if the number is at least 10
bold = bold and "!" or "|"
if cur_num >= 10 then
return ('%s %sclass="table-cell %s | %s\n'):format(bold, colspan, class_name, contents)
local down_num = cur_num / 10
end
local down_data = m_data.numbers[down_num]
 
local has_outer_display = not not (prev_outer_display or next_outer_display)
if down_data and down_data[cur_type] then
local function format_upper_lower_display_row(display)
if type(down_data[cur_type]) == "table" then
local blank_cell
down_display = down_num .. multiple_num_links(remove_suffix and unsuffix(down_data[cur_type]) or down_data[cur_type], lang)
if has_outer_display then
else
blank_cell = '| colspan="2" |\n'
down_display = full_link({lang = lang, term = remove_suffix and unsuffix(down_data[cur_type]) or down_data[cur_type], alt = down_num, tr = "-"})
else
end
blank_cell = "|\n"
end
end
end
local parts = {'|- class="adjacent-panel"\n'}
insert(parts, blank_cell)
insert(parts, format_cell(display, "adjacent-number"))
insert(parts, blank_cell)
return concat(parts)
end
end
 
local canonical_name = lang:getCanonicalName()
upper_display = upper_display and format_upper_lower_display_row(upper_display) or ""
local appendix1 = canonical_name .. ' numerals'
lower_display = lower_display and format_upper_lower_display_row(lower_display) or ""
local appendix2 = canonical_name .. ' numbers'
 
local appendix
local function format_display_cell(display)
local title
return format_cell(display, "adjacent-number")
if mw.title.new(appendix1, "Appendix").exists then
appendix = appendix1
elseif mw.title.new(appendix2, "Appendix").exists then
appendix = appendix2
end
end
 
if appendix then
prev_display = format_display_cell(prev_display)
title = '[[Appendix:' .. appendix .. '|' .. appendix2 .. ']]'
next_display = format_display_cell(next_display)
prev_outer_display = has_outer_display and format_display_cell(prev_outer_display or "") or ""
next_outer_display = has_outer_display and format_display_cell(next_outer_display or "") or ""
cur_display = format_cell(cur_display, "current-number", nil, "bold")
 
local forms_display = ('| colspan="%s" style="text-align: center;" | %s\n'):format(
has_outer_display and 5 or 3, concat(formatted_forms, "<br/>"))
 
local footer_display
if cur_data.wplink then
local footer =
"[[w:" .. lang:getCode() .. ":|" .. lang:getCanonicalName() .. " Wikipedia]] article on " ..
m_links.full_link{lang = lang, term = "w:" .. lang:getCode() .. ":" .. cur_data.wplink,
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)
else
else
title = appendix2
footer_display = ""
end
end
 
local edit_link = ' <sup>(<span class="plainlinks">[' ..
local edit_link = ' <sup>(<span class="plainlinks">[' ..
tostring(mw.uri.fullUrl(module_name, { action = "edit" })) ..
tostring(mw.uri.fullUrl(module_name, { action = "edit" })) ..
" edit]</span>)</sup>"
" edit]</span>)</sup>"
return [=[{| class="floatright" cellpadding="5" cellspacing="0" style="background: #ffffff; border: 1px #aaa solid; border-collapse: collapse; margin-top: .5em;" rules="all"
|+ ''']=] .. title .. edit_link .. "'''" ..
(up_display and [=[


|- style="text-align: center; background:#dddddd;"
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" ..
| style="font-size:smaller;" | ]=] .. up_display .. [=[
upper_display .. '|- style="text-align: center;"\n' ..
prev_outer_display .. prev_display .. cur_display .. next_display .. next_outer_display .. "|-\n" ..
lower_display .. "|-\n" ..
forms_display .. footer_display .. "|}" ..
require("Module:TemplateStyles")("Template:number box/styles.css")
end
 
 
function export.show_box_manual(frame)
local m_links = require("Module:links")
local num_type = frame.args["type"]
 
local args = require("Module:parameters").process(frame:getParent().args, {
[1] = {required = true, type = "language", default = "und"},
sc = {type = "script"},
headlink = true,
wplink = true,
alt = true,
tr = true,
[2] = true, -- prev_symbol
[3] = true, -- cur_symbol
[4] = true, -- next_symbol
[5] = true, -- prev_term
[6] = true, -- next_term
card = true, cardalt = true, cardtr = true,
ord = true, ordalt = true, ordtr = true,
adv = true, advalt = true, advtr = true,
mult = true, multalt = true, multtr = true,
dis = true, disalt = true, distr = true,
coll = true, collalt = true, colltr = true,
frac = true, fracalt = true, fractr = true,
opt = true, optx = true, optxalt = true, optxtr = true,
opt2 = true, opt2x = true, opt2xalt = true, opt2xtr = true,
})
 
local lang = args[1]
local sc = args.sc
local headlink = args.headlink
local wplink = args.wplink
local alt = args.alt
local tr = args.tr


|
local prev_symbol = args[2]
]=] or "\n") .. [=[|- style="text-align: center;"
local cur_symbol = args[3]
| style="min-width: 6em; font-size:smaller; background:#dddddd;" | ]=] .. prev_display .. [=[
local next_symbol = args[4]


! style="min-width: 6em; font-size:larger;" | ]=] .. cur_display .. [=[
local prev_term = args[5]
local next_term = args[6]


| style="min-width: 6em; font-size:smaller; background:#dddddd;" | ]=] .. next_display .. [=[
local cardinal_term = args.card; local cardinal_alt = args.cardalt; local cardinal_tr = args.cardtr


]=] .. (down_display and [=[|- style="text-align: center; background:#dddddd;"
local ordinal_term = args.ord; local ordinal_alt = args.ordalt; local ordinal_tr = args.ordtr
|
| style="font-size:smaller;" | ]=] .. down_display .. [=[


|
local adverbial_term = args.adv; local adverbial_alt = args.advalt; local adverbial_tr = args.advtr
]=] or "") .. [=[|-
| colspan="3" | ]=] .. table.concat(forms, "<br/>") .. [=[


|}]=]
local multiplier_term = args.mult; local multiplier_alt = args.multalt; local multiplier_tr = args.multtr
end


local distributive_term = args.dis; local distributive_alt = args.disalt; local distributive_tr = args.distr


local trim = mw.text.trim
local collective_term = args.coll; local collective_alt = args.collalt; local collective_tr = args.colltr


-- Assumes string or nil (or false), the types that can be found in an args table.
local fractional_term = args.frac; local fractional_alt = args.fracalt; local fractional_tr = args.fractr
local function if_not_empty(val)
if val and trim(val) == "" then
return nil
else
return val
end
end


local optional1_title = args.opt
local optional1_term = args.optx; local optional1_alt = args.optxalt; local optional1_tr = args.optxtr


function export.show_box_manual(frame)
local optional2_title = args.opt2
local m_links = require("Module:links")
local optional2_term = args.opt2x; local optional2_alt = args.opt2xalt; local optional2_tr = args.opt2xtr
local num_type = frame.args["type"]
local args = {}
--cloning parent's args while also assigning nil to empty strings
for pname, param in pairs(frame:getParent().args) do
args[pname] = if_not_empty(param)
end
local sc = args["sc"];  
local headlink = args["headlink"]
local wplink = args["wplink"]
local alt = args["alt"]
local tr = args["tr"]
local prev_symbol = if_not_empty(args[1])
local cur_symbol = if_not_empty(args[2]);
local next_symbol = if_not_empty(args[3])
local prev_term = if_not_empty(args[4])
local next_term = if_not_empty(args[5])


local cardinal_term = args["card"]; local cardinal_alt = args["cardalt"]; local cardinal_tr = args["cardtr"]
track(lang:getCode())
local ordinal_term = args["ord"]; local ordinal_alt = args["ordalt"]; local ordinal_tr = args["ordtr"]
local adverbial_term = args["adv"]; local adverbial_alt = args["advalt"]; local adverbial_tr = args["advtr"]
local multiplier_term = args["mult"]; local multiplier_alt = args["multalt"]; local multiplier_tr = args["multtr"]
local distributive_term = args["dis"]; local distributive_alt = args["disalt"]; local distributive_tr = args["distr"]
local collective_term = args["coll"]; local collective_alt = args["collalt"]; local collective_tr = args["colltr"]
local fractional_term = args["frac"]; local fractional_alt = args["fracalt"]; local fractional_tr = args["fractr"]
local optional1_title = args["opt"]
local optional1_term = args["optx"]; local optional1_alt = args["optxalt"]; local optional1_tr = args["optxtr"]
local optional2_title = args["opt2"]
local optional2_term = args["opt2x"]; local optional2_alt = args["opt2xalt"]; local optional2_tr = args["opt2xtr"]


sc = (sc or nil)
if sc then
if sc then
require("Module:debug").track("number list/sc")
track("sc")
end
end
 
if headlink then
if headlink then
require("Module:debug").track("number list/headlink")
track("headlink")
end
end
 
if wplink then
if wplink then
require("Module:debug").track("number list/wplink")
track("wplink")
end
end
 
if alt then
if alt then
require("Module:debug").track("number list/alt")
track("alt")
end
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
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
require("Module:debug").track("number list/xalt")
track("xalt")
end
end
local subpage = mw.loadData("Module:headword/data").pagename
local is_reconstructed = lang:hasType("reconstructed") or mw.title.getCurrentTitle().nsText == "Reconstruction"
local subpage = mw.title.getCurrentTitle().subpageText
-- Commenting out this line prevents passing redundant alts to full_link;
local is_reconstructed = lang_type == "reconstructed" or mw.title.getCurrentTitle().nsText == "Reconstruction"
-- however, there may have been a purpose to it.
alt = alt or (is_reconstructed and "*" or "") .. subpage
-- alt = alt or (is_reconstructed and "*" or "") .. subpage
 
if num_type == "cardinal" then
if num_type == "cardinal" then
cardinal_term = (is_reconstructed and "*" or "") .. subpage
cardinal_term = cardinal_term or (is_reconstructed and "*" or "") .. subpage
cardinal_alt = alt
cardinal_alt = cardinal_alt or alt
cardinal_tr = tr
cardinal_tr = cardinal_tr or tr
elseif num_type == "ordinal" then
elseif num_type == "ordinal" then
ordinal_term = (is_reconstructed and "*" or "") .. subpage
ordinal_term = ordinal_term or (is_reconstructed and "*" or "") .. subpage
ordinal_alt = alt
ordinal_alt = ordinal_alt or alt
ordinal_tr = tr
ordinal_tr = ordinal_tr or tr
end
end
 
local header = "Avendonian" .. " " .. num_type .. " numbers"
local header = lang:getCanonicalName() .. " " .. num_type .. " numbers"
 
if headlink then
if headlink then
header = "[[" .. headlink .. "|" .. header .. "]]"
header = "[[" .. headlink .. "|" .. header .. "]]"
end
end
 
local previous = ""
local previous = ""
 
if prev_term or prev_symbol then
if prev_term or prev_symbol then
previous = m_links.full_link({lang = lang, sc = sc, term = prev_term, alt = "&nbsp;&lt;&nbsp;&nbsp;" .. prev_symbol, tr = "-"})
previous = m_links.full_link{lang = lang, sc = sc, term = prev_term, alt = "&nbsp;&lt;&nbsp;&nbsp;" .. prev_symbol, tr = "-", no_alt_ast = true}
end
end
 
local current = m_links.full_link({lang = lang, sc = sc, alt = cur_symbol, tr = "-"})
local current = m_links.full_link{lang = lang, sc = sc, alt = cur_symbol, tr = "-", no_alt_ast = true}
 
local next = ""
local next = ""
 
if next_term or next_symbol then
if next_term or next_symbol then
next = m_links.full_link({lang = lang, sc = sc, term = next_term, alt = next_symbol .. "&nbsp;&nbsp;&gt;&nbsp;", tr = "-"})
next = m_links.full_link{lang = lang, sc = sc, term = next_term, alt = next_symbol .. "&nbsp;&nbsp;&gt;&nbsp;", tr = "-", no_alt_ast = true}
end
end
 
local forms = {}
local forms = {}
 
if cardinal_term then
if cardinal_term then
table.insert(forms, " &nbsp;&nbsp;&nbsp; ''[[cardinal number|Cardinal]]'' : " .. m_links.full_link({lang = lang, sc = sc, term = cardinal_term, alt = cardinal_alt, tr = cardinal_tr}))
insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:cardinal number|Cardinal]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = cardinal_term, alt = cardinal_alt, tr = cardinal_tr})
end
end
 
if ordinal_term then
if ordinal_term then
table.insert(forms, " &nbsp;&nbsp;&nbsp; ''[[ordinal number|Ordinal]]'' : " .. m_links.full_link({lang = lang, sc = sc, term = ordinal_term, alt = ordinal_alt, tr = ordinal_tr}))
insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:ordinal number|Ordinal]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = ordinal_term, alt = ordinal_alt, tr = ordinal_tr})
end
end
 
if adverbial_term then
if adverbial_term then
table.insert(forms, " &nbsp;&nbsp;&nbsp; ''[[adverbial number|Adverbial]]'' : " .. m_links.full_link({lang = lang, sc = sc, term = adverbial_term, alt = adverbial_alt, tr = adverbial_tr}))
insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:adverbial number|Adverbial]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = adverbial_term, alt = adverbial_alt, tr = adverbial_tr})
end
end
 
if multiplier_term then
if multiplier_term then
table.insert(forms, " &nbsp;&nbsp;&nbsp; ''[[multiplier|Multiplier]]'' : " .. m_links.full_link({lang = lang, sc = sc, term = multiplier_term, alt = multiplier_alt, tr = multiplier_tr}))
insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:multiplier|Multiplier]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = multiplier_term, alt = multiplier_alt, tr = multiplier_tr})
end
end
 
if distributive_term then
if distributive_term then
table.insert(forms, " &nbsp;&nbsp;&nbsp; ''[[distributive number|Distributive]]'' : " .. m_links.full_link({lang = lang, sc = sc, term = distributive_term, alt = distributive_alt, tr = distributive_tr}))
insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:distributive number|Distributive]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = distributive_term, alt = distributive_alt, tr = distributive_tr})
end
end
 
if collective_term then
if collective_term then
table.insert(forms, " &nbsp;&nbsp;&nbsp; ''[[collective number|Collective]]'' : " .. m_links.full_link({lang = lang, sc = sc, term = collective_term, alt = collective_alt, tr = collective_tr}))
insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:collective number|Collective]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = collective_term, alt = collective_alt, tr = collective_tr})
end
end
 
if fractional_term then
if fractional_term then
table.insert(forms, " &nbsp;&nbsp;&nbsp; ''[[fractional|Fractional]]'' : " .. m_links.full_link({lang = lang, sc = sc, term = fractional_term, alt = fractional_alt, tr = fractional_tr}))
insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:fractional|Fractional]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = fractional_term, alt = fractional_alt, tr = fractional_tr})
end
end
 
if optional1_title then
if optional1_title then
table.insert(forms, " &nbsp;&nbsp;&nbsp; ''" .. optional1_title .. "'' : " .. m_links.full_link({lang = lang, sc = sc, term = optional1_term, alt = optional1_alt, tr = optional1_tr}))
insert(forms, " &nbsp;&nbsp;&nbsp; ''" .. optional1_title .. "'' : " .. m_links.full_link{lang = lang, sc = sc, term = optional1_term, alt = optional1_alt, tr = optional1_tr})
end
end
 
if optional2_title then
if optional2_title then
table.insert(forms, " &nbsp;&nbsp;&nbsp; ''" .. optional2_title .. "'' : " .. m_links.full_link({lang = lang, sc = sc, term = optional2_term, alt = optional2_alt, tr = optional2_tr}))
insert(forms, " &nbsp;&nbsp;&nbsp; ''" .. optional2_title .. "'' : " .. m_links.full_link{lang = lang, sc = sc, term = optional2_term, alt = optional2_alt, tr = optional2_tr})
end
end
 
local footer = ""
local footer = ""
 
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
 
return [=[{| class="floatright" cellpadding="5" cellspacing="0" style="background: #ffffff; border: 1px #aaa solid; border-collapse: collapse; margin-top: .5em;" rules="all"  
return [=[{| class="floatright number-box" cellpadding="5" cellspacing="0" rules="all"
|+ ''']=] .. header .. [=['''
|+ ''']=] .. header .. [=['''
|-
|-
| style="width: 64px; background:#dddddd; text-align: center; font-size:smaller;" | ]=] .. previous .. [=[
| class="adjacent-slot" | ]=] .. previous .. [=[


! style="width: 98px; text-align: center; font-size:larger;" | ]=] .. current .. [=[
! class="current-slot" | ]=] .. current .. [=[


| style="width: 64px; text-align: center; background:#dddddd; font-size:smaller;" | ]=] .. next .. [=[
| class="adjacent-slot" | ]=] .. next .. [=[


|-
|-
| colspan="3" | ]=] .. table.concat(forms, "<br/>") .. [=[
| colspan="3" class="form-slot" | ]=] .. concat(forms, "<br/>") .. [=[


|-
|-
| colspan="3" style="text-align: center; background: #dddddd;" | ]=] .. footer .. [=[
| colspan="3" class="footer-slot" | ]=] .. footer .. [=[


|}]=]
|}]=] .. require("Module:TemplateStyles")("Template:number box/styles.css")
end
end


return export
return export

Latest revision as of 14:02, 8 May 2026



local m_links = require("Module:links")
local m_str_utils = require("Module:string utilities")

local char = string.char
local concat = table.concat
local gsub = m_str_utils.gsub
local insert = table.insert
local list_to_set = require("Module:table").listToSet
local sort = table.sort
local split = m_str_utils.split
local u = m_str_utils.char
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local upper = string.upper

local export = {}
local decimal_strategy

--[=[

Terminology:

Number = a bare number; a mathematical entity which has different form types (e.g. cardinal, ordinal)
Form type = a category of the forms that represent a number; examples are cardinal, ordinal, distributive, fractional
Form = a word or expression that represents a number in a given language
Tag = an identifier attached to a form that allows different logical subtypes of forms from the same form type to be
      identified; e.g. 'vuitanta-vuit<tag:Central>' vs. 'huitanta-huit<tag:Valencian>' to identify variants of
	  Catalan cardinal number 88 for different dialectal standards; there can be multiple tags per form, e.g.
	  'tair ar ddeg<tag:vigesimal><tag:feminine>' for the Welsh number 13 where there are both decimal/vigesimal and
	  masculine/feminine variants of this number
Tag list = a list of tags in the order they are specified in the data, e.g. {"vigesimal", "feminine"} for the example
           above
Combined tag = the string representation of a tag list, using ||| to separate individual tags
]=]

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

-- Keys in a `numbers` entry that aren't form types.
local non_form_types = {
	numeral = true,
	wplink = true,
	next = true,
	prev = true,
	next_outer = true,
	prev_outer = true,
	upper = true,
	lower = true,
}

local function track(page)
	require("Module:debug/track")("number list/" .. page)
	return true
end

--[=[
--
-- General set intersection
local function set_intersection(sets)
	local intersection = {}
	for key, _ in pairs(sets[1]) do
		intersection[key] = true
	end
	for i = 2, #sets do
		local this_set = sets[i]
		for key, _ in pairs(intersection) do
			if not this_set[key] then
				-- See https://stackoverflow.com/questions/6167555/how-can-i-safely-iterate-a-lua-table-while-keys-are-being-removed
				-- It is safe to modify or remove a key while iterating over the table.
				intersection[key] = nil
			end
		end
	end
	return intersection
end
]=]

local function set_intersection(set1, set2)
	local intersection = {}
	for key, _ in pairs(set1) do
		intersection[key] = true
	end
	for key, _ in pairs(intersection) do
		if not set2[key] then
			-- See https://stackoverflow.com/questions/6167555/how-can-i-safely-iterate-a-lua-table-while-keys-are-being-removed
			-- It is safe to modify or remove a key while iterating over the table.
			intersection[key] = nil
		end
	end
	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

function export.get_data_module_name(langcode)
	return "Module:number list/data/" .. langcode
end

-- Format a number (either a Lua number or a string) in fixed point without any decimal point or scientific notation.
-- `tostring()` doesn't work because it converts large numbers such as 1000000000000000 to "1e+15".
function export.format_fixed(number)
	if type(number) == "string" then
		return number
	else
		return ("%.0f"):format(number)
	end
end

-- 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
-- `form` for the form, and `tr`, `tag`, `q`, `qq`, `g` or `link` for the modifiers. The `tag` field is a tag list
-- (see above).
function export.parse_form_and_modifiers(form_with_modifiers)
	local retval = {}
	local form
	form = form_with_modifiers
	while true do
		local new_form, angle_bracketed = form:match("^(.-)(%b<>)$")
		if not new_form then
			break
		end
		local prefix, content = angle_bracketed:match "^<(%w+):(.+)>$"
		if not prefix then
			break
		end
		if prefix == "tag" then
			if retval.tag then
				insert(retval.tag, content)
			else
				retval.tag = {content}
			end
		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
				error(("Duplicate modifier '%s' in data module form, already saw value '%s': %s"):format(prefix,
					retval[prefix], form_with_modifiers))
			else
				retval[prefix] = content
			end
		else
			error(("Unrecognized modifier '%s' in data module form: %s"):format(prefix, form_with_modifiers))
		end
		form = new_form
	end
	retval.form = form
	return retval
end

-- Find the `numbers` object for a given number (which should be in string representation).
function export.lookup_data(m_data, numstr)
	-- Don't try to convert very large numbers to Lua numbers because they may overflow.
	-- Powers of 10 >= 10^22 cannot be represented exactly as a Lua number.
	return m_data.numbers[numstr] or #numstr < 22 and m_data.numbers[tonumber(numstr)] or nil
end

-- Return true if a < b, where either may be a Lua number or the string representation of a number.
function export.numbers_less_than(a, b)
	a, b = export.format_fixed(a), export.format_fixed(b)
	local alen = #a
	local blen = #b
	if alen < blen then
		return true
	end
	if alen > blen then
		return false
	end
	return a < b
end

-- Return true if a > b, where either may be a Lua number or the string representation of a number.
function export.numbers_greater_than(a, b)
	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

-- Given a number form, convert it to its independent (un-affixed) form. This only makes sense for certain languages
-- where there is a difference between independent and affixed forms of numerals. Currently the only such language
-- is Swahili, where e.g. the cardinal number form for 3 is affixed [[-tatu]], independent [[tatu]], and the ordinal
-- number form is [[-a tatu]], independent [[tatu]]. We rely on a set of Lua pattern substitutions to convert from
-- affixed to independent form.
--
-- FIXME: This needs major rethinking in a way that isn't specific to Swahili.
local function maybe_unaffix(m_data, form)
	if not m_data.unaffix then
		return form
	end
	for _, entry in ipairs(m_data.unaffix) do
		local from, to = unpack(entry)
		form = gsub(form, from, to)
	end
	return form
end

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

-- Return true if the given number form object (taken from the data for `lang`, after parsing the form for modifiers)
-- matches `pagename`. If there is a <link:...> modifier, we check against it. Otherwise, we check against the form
-- itself. In this case, the form may have links and/or accent/length marks that need to be stripped, and we may need
-- to convert the form to its independent (un-affixed) form, if there is a difference between independent and affixed
-- forms (as in Swahili).
local function form_equals_pagename(formobj, pagename, m_data, lang)
	if formobj.link == pagename then
		return true
	end
	local stripped_form = form_to_stripped_form(formobj.form, lang)
	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

-- 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).
local function get_next_and_prev_keys(m_data, numstr, strategy, lookup_data)
	local numdata = lookup_data(numstr)
	if not numdata then
		return nil, nil
	end
	local nextnum = numdata.next
	local prevnum = numdata.prev
	if not nextnum or not prevnum then
		-- Find the next/previous numbers by sorting all the keys and locating the number in question among them.
		local sorted_list = {}
		local seen = {}
		local index = 1
		for key, _ in pairs(m_data.numbers) do
			local normalized_key = strategy.normalize_data_key(key)
			if not seen[normalized_key] then
				seen[normalized_key] = true
				sorted_list[index] = normalized_key
				index = index + 1
			end
		end

		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
		-- matter to search linearly.
		for i, key in ipairs(sorted_list) do
			if key == numstr then
				nextnum = nextnum or sorted_list[i + 1]
				prevnum = prevnum or sorted_list[i - 1]
				break
			end
		end
	end

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

	return nextnum, prevnum
end

-- Find the "description objects" (a two-element list {NUMBER, TYPE}, where NUMBER is either a Lua number or a string,
-- depending on how it appears in the underlying data) that matches `pagename` and (if given) `matching_type`.
-- Return a list of such objects.
local function lookup_number_by_form(lang, m_data, pagename, matching_type)
	local retval = {}
	local function check_form(form, num, typ)
		local formobj = export.parse_form_and_modifiers(form)
		if form_equals_pagename(formobj, pagename, m_data, lang) and (not matching_type or typ == matching_type) then
			-- It's possible the same pagename occurs multiply for a given type and number, e.g. with different length
			-- or accent marks. The calling code is OK with multiple entries for a given number (which can also occur
			-- with different types, e.g. the ordinal and fractional forms for a given number are the same), but will
			-- throw an error if different numbers are seen.
			insert(retval, {num, typ})
		end
	end

	for num, numdata in pairs(m_data.numbers) do
		for numtype, forms in pairs(numdata) do
			if non_form_types[numtype] then
				-- do nothing
			elseif type(forms) == "table" then
				for _, form in ipairs(forms) do
					check_form(form, num, numtype)
				end
			else
				check_form(forms, num, numtype)
			end
		end
	end

	return retval
end

-- additional_types is an array of tables like form_types,
-- but each table can contain the keys "before" or "after", which specify
-- the numeral type that the form should appear before or after.
-- The transformations are applied in order.
local function add_form_types(form_types, additional_types)
	local types = require("Module:table").deepCopy(form_types)
	for _, additional_type in ipairs(additional_types) do
		if not (additional_type.before or additional_type.after) then
			insert(types, additional_type)
		else
			if additional_type.before and additional_type.after then
				error("The form type '" .. additional_type.key .. "' is specifying both before and after, which is not allowed")
			end

			local anchor, index = additional_type.before or additional_type.after

			for i, another_type in ipairs(types) do
				if another_type.key == anchor then
					index = i
					break
				end
			end

			if index and additional_type.after then
				index = index + 1
			end

			additional_type = require("Module:table").shallowCopy(additional_type)
			additional_type.before, additional_type.after = nil, nil
			
			if not index then
				mw.log("Number type "
					.. (additional_type.before or additional_type.after)
					.. " was not found.")
				insert(types, additional_type)
			else
				insert(types, index, additional_type)
			end
		end
	end
	return types
end

-- Return all form types for the language in question, in order.
function export.get_number_types(m_data)
	local form_types = default_form_types
	if m_data.additional_number_types then
		return add_form_types(form_types, m_data.additional_number_types)
	else
		return form_types
	end
end

-- Convert a number type object (an object with `display` and `key` fields) to its displayed form.
function export.display_number_type(number_type)
	if number_type.display then
		return number_type.display
	else
		return (number_type.key:gsub("^.", upper):gsub("_", " "))
	end
end

-- Group digits with a separator, such as a comma or a period. See [[w:Digit grouping]].
local function add_separator(numstr, separator, group, start)
	start = start or group
	if start >= #numstr then
		return numstr
	end

	local parts = { numstr:sub(-start) }
	for i = start + 1, #numstr, group do
		insert(parts, 1, numstr:sub(-(i + group - 1), -i))
	end

	return concat(parts, separator)
end

function export.add_thousands_separator(numstr, separator)
	if #numstr < 4 then -- < 1000
		return numstr
	end
	return add_separator(numstr, separator or ",", 3)
end

local function add_Indic_separator(numstr, separator)
	return add_separator(numstr, separator, 2, 3)
end

-- Convert a number (represented as a string) to non-Arabic form based on the specs in `numeral_config`.
-- This is used, for example, to display the Hindu, Eastern Arabic or Roman form of a number along with the standard
-- Arabic form. Most of the code below assumes that the non-Arabic numerals are decimal, and the digits map one-to-one
-- with Arabic numerals. If this is not the case (e.g. for Roman numerals), a special module function is called to do
-- the conversion.
function export.generate_non_arabic_numeral(numeral_config, numstr)
	-- `numstr` is a number represented as a string. See comment near top of show_box().
	if numeral_config.module and numeral_config.func then
		return require("Module:" .. numeral_config.module)[numeral_config.func](numstr)
	end

	local thousands_separator, Indic_separator, zero_codepoint =
		numeral_config.thousands_separator,
		numeral_config.Indic_separator,
		numeral_config.zero_codepoint

	if not zero_codepoint then
		return nil
	end

	if thousands_separator then
		numstr = export.add_thousands_separator(numstr, thousands_separator)
	elseif Indic_separator then
		numstr = add_Indic_separator(numstr, Indic_separator)
	end

	return numstr:gsub("%d", function (digit)
		return u(zero_codepoint + tonumber(digit))
	end)
end


-- Format a number (either a Lua number or a string) for display. Sufficiently small numbers are displayed in fixed
-- point with thousands separators. Larger numbers are displayed in both fixed point and scientific notation using
-- superscripts, and sufficiently large numbers are displayed only in scientific notation.
function export.format_number_for_display(number)
	local MAX_NUM_DIGITS_FOR_FIXED_ONLY = 6
	local MIN_NUM_DIGITS_FOR_SCIENTIFIC_ONLY = 13
	local numstr = export.format_fixed(number)
	local fixed = export.add_thousands_separator(numstr)
	if #numstr <= MAX_NUM_DIGITS_FOR_FIXED_ONLY then
		return fixed
	end
	local kstr = numstr:match("^([0-9]*[1-9])0*$")
	if not kstr then
		error("Internal error: Unable to match number '" .. numstr .. "'")
	end
	local exponent = ("10<sup>%s</sup>"):format(#numstr - 1)
	local mantissa
	if kstr == "1" then
		mantissa = ""
	elseif #kstr == 1 then
		mantissa = kstr .. " × "
	else
		mantissa = kstr:gsub("^([0-9])", "%1.") .. " × "
	end
	local scientific = mantissa .. exponent
	if #numstr >= MIN_NUM_DIGITS_FOR_SCIENTIFIC_ONLY then
		return scientific
	else
		return fixed .. " (" .. scientific .. ")"
	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

-- Map a list of tags to a single string that is equivalent. We need to do this because we can't easily put lists in the
-- keys of tables.
local function tag_list_to_combined_tag(tag_list)
	return concat(tag_list, "|||")
end

-- Given a list of forms with attached inline modifiers (e.g. 'huitanta-huit<tag:Valencian>' or
-- 'tair ar ddeg<tag:vigesimal><tag:feminine>'), parse the forms into form objects (the return value of
-- parse_form_and_modifiers()) and group by the tag. Three values are returned:
-- `seen_forms`, `forms_by_tag`, `seen_tags` where:
-- (1) `seen_forms` is the list of parsed form objects;
-- (2) `forms_by_tag` is a table grouping the form objects by combined tag, where the key is the tag and the value is
--      a list of the form objects seen with that tag (forms without tag are grouped under the empty-string tag);
-- (3) `seen_tags` is a list of the combined tags encountered, in the order they were encountered;
-- (4) `combined_tags_to_tag_lists` is a map from combined tags to the corresponding tag lists.
function export.group_numeral_forms_by_tag(forms)
	local seen_forms = {}
	local forms_by_tag = {}
	local seen_tags = {}
	local combined_tags_to_tag_lists = {}

	for _, form in ipairs(forms) do
		local formobj = export.parse_form_and_modifiers(form)
		insert(seen_forms, formobj)
		local combined_tag = formobj.tag and tag_list_to_combined_tag(formobj.tag) or ""
		if not forms_by_tag[combined_tag] then
			insert(seen_tags, combined_tag)
			forms_by_tag[combined_tag] = {}
			combined_tags_to_tag_lists[combined_tag] = formobj.tag or {}
		end
		insert(forms_by_tag[combined_tag], formobj)
	end

	return seen_forms, forms_by_tag, seen_tags, combined_tags_to_tag_lists
end

-- Given a form object (as returned by parse_form_and_modifiers()), format as appropriate for the current language.
function export.format_formobj(formobj, m_data, lang)
	local left_q = formobj.q and require("Module:qualifier").format_qualifier(formobj.q) .. " " 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 alt = formobj.alt
	if not alt and term ~= formobj.form then
		alt = formobj.form
	end
	return left_q .. m_links.full_link{
		lang = lang, term = term, alt = alt, tr = formobj.tr, id = formobj.id,
	} .. right_q
end

-- Implementation of {{number box}}.
function export.show_box(frame)
	local full_link = m_links.full_link

	local params = {
		[1] = {required = true, type = "language", default = "und"},
		[2] = true,
		["pagename"] = true,
		["type"] = true,
	}

	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 lang = args[1]
	local langcode = lang:getCode()

	-- Get the data from the data module. Some modules (e.g. currently [[Module:number list/data/ka]]) have to be
	-- loaded with require() because the exported numbers table has a metatable.
	local module_name = export.get_data_module_name(langcode)
	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.loadData("Module:headword/data").pagename
	local cur_type = args.type

	-- We represent all numbers as strings in this function to deal with the limited precision inherent in Lua numbers.
	-- These large numbers do occur, such as 100 trillion ([[རབ་བཀྲམ་ཆེན་པོ]]), 1 sextillion, etc. Lua represents all
	-- numbers as 64-bit floats, meaning that some numbers above 2^53 cannot be represented exactly. The first power of
	-- 10 that cannot be represented exactly is 10^22 (ten sextillion in short scale, ten thousand trillion in long
	-- scale), but the first power of ten whose neighboring numbers cannot be represented exactly is 10^16 (ten
	-- quadrillion or ten thousand billion). Ideally we would use a big integer library of some kind, but unfortunately
	-- Wiktionary does not seem to have any such library installed. MediaWiki docs make mention of bcmath, but
	-- mw.bcmath.new() throws an error.
	--
	-- In module data, we allow numbers to be indexed as Lua numbers or as strings. See lookup_data() above.
	local cur_num = args[2] or langcode == "und" and mw.title.getCurrentTitle().nsText == "Template" and "2" or nil

	-- If a current number wasn't specified, find it by looking through the data for the current language and matching
	-- forms against the pagename.
	if not cur_num then
		local nums_and_types = lookup_number_by_form(lang, m_data, pagename, cur_type)
		if #nums_and_types == 0 then
			error("The current page name '" .. pagename .. "' does not match the spelling of any known number in [[" ..
				module_name .. "]]. Check the data module or the spelling of the page.")
		end
		for _, num_and_type in ipairs(nums_and_types) do
			local num = num_and_type[1]
			num = number_system.normalize_data_key(num)
			if cur_num and num ~= cur_num then
				local errparts = {}
				for _, num_and_type in ipairs(nums_and_types) do
					local num, typ = unpack(num_and_type)
					num = number_system.normalize_data_key(num)
					insert(errparts, ("%s (%s)"):format(num, typ))
				end
				error("The current page name '" .. pagename .. "' matches the spelling of multiple numbers in [[" ..
					module_name .. "]]: " .. concat(errparts, ",") .. ". Please specify the number explicitly.")
			else
				cur_num = num
			end
		end
	end

	local function candidate_matches_pagename(candidate_num)
		local candidate_data = number_system.lookup_data(m_data, candidate_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

	-- Wrapper around `export.lookup_data` that may throw an error if the number can't be found (specifically if
	-- param_for_error is given).
	local function lookup_data(numstr, param_for_error)
		numstr = number_system.normalize_data_key(numstr)
		local retval = number_system.lookup_data(m_data, numstr)
		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 '
				.. "[[%s]]; please fix the module."):format(param_for_error, numstr, cur_num, module_name))
		end
		return retval
	end

	local cur_data = lookup_data(cur_num)
	if not cur_data then
		error('The number "' .. cur_num .. '" is not found in the "numbers" table in [[' .. module_name .. "]].")
	end

	local formatted_forms = {}

	if cur_type and not cur_data[cur_type] then
		error("The numeral type " .. cur_type .. " for " .. cur_num .. " is not found in [[" .. module_name .. "]].")
	end

	-- See above for the definition of "combined tag" and "tag list". The combined tag is just the concatenation of the
	-- tag list with ||| between the tags.
	local cur_tag_list, cur_combined_tag

	local form_types = export.get_number_types(m_data)
	
	-- LONG COMMENT EXPLAINING TAG HANDLING:
	--
	-- For each form type (see `form_types` at top of file), group the entries for that form type by tag and figure out
	-- what the current form type and tag is, i.e. the form type and tag for the form matching the pagename. Tags are
	-- e.g. as in 'vuitanta-vuit<tag:Central>' or 'huitanta-huit<tag:Valencian>' for Catalan and allow different
	-- logical sets of numbers for the same form type to be identified. There can potentially be multiple tags per
	-- form, e.g. 'tair ar ddeg<tag:vigesimal><tag:feminine>' for the Welsh number 13 where there are both decimal/
	-- vigesimal and masculine/feminine variants of this number.
	--
	-- We need to do two passes over all form types. In the first pass, for each form type we parse all the forms,
	-- group them by tag, and store the results in a per-form-type table. In the second pass, we then format all forms
	-- for all form types. The reason for doing two passes is because we need to know the current tag in order to
	-- display a form type correctly (because we display the forms for the current tag before the forms for any other
	-- tags), but we won't know the current tag until we have done a pass over all form types and forms of those form
	-- types in order to determine which one matches the pagename.
	--
	-- We use the current tag in two ways:
	-- 1. When displaying all the forms for a given number, we group both by form type and tag, and display the forms
	--    for a given form type/tag combination on a single line. For a given form type, we display the forms for each
	--    tag in the order the tags were specified in the data, except that the forms for the current tag are placed
	--    before all others (so e.g. for Catalan, if the current tag is "Valencian", we list the Valencian form(s)
	--    first even if the Central form(s) are listed first in the data file).
	-- 2. When displaying links to adjacent numbers in display_adjacent_number_links(), if there aren't form(s) for the
	--    current type, we don't display any links; but if there are mutiple tagged forms for the current type, we only
	--    display links for the forms for the current tag if there are any such forms, otherwise we display links for
	--    all forms of all tags.
	--
	-- In the presence of multiple tags, things get a bit more complicated:
	-- 1. When displaying links to adjacent numbers, say the current tag is vigesimal+feminine, we want to prefer an
	--    adjacent-number form that's both vigesimal and feminine, but otherwise we prefer one that's vigesimal or
	--    feminine over one that's neither. Say the current tag is just vigesimal; we of course prefer an
	--    adjacent-number form that's just vigesimal, but otherwise we prefer a tag that's vigesimal + either masculine
	--    or feminine to a tag that's not vigesimal. So it seems we want the form(s) that have the maximum intersection
	--    of tags, and if there are two different tag lists with the same number of intersecting tags (e.g. the current
	--    tag is vigesimal+feminine and we have a choice of decimal+feminine or just vigesimal), we should prefer the
	--    form that has fewer non-matching tags, hence we prefer the just-vigesimal form.
	-- 2. By the same logic, when displaying all the forms for a given number, we should order by the size of the
	--    intersection of the tag list in question with the current tag list, then inversely by the size of the tag list
	--    (so we prefer tag lists with fewer non-matching tags), then by the order of the tag lists in the data file.

	local forms_by_tag_per_form_type = {}
	local seen_tags_per_form_type = {}
	local combined_tags_to_tag_lists_per_form_type = {}

	for _, form_type in ipairs(form_types) do
		local numeral = cur_data[form_type.key]
		if numeral then
			local seen_forms, forms_by_tag, seen_tags, combined_tags_to_tag_lists = export.group_numeral_forms_by_tag(
				type(numeral) == "table" and numeral or {numeral}
			)
			forms_by_tag_per_form_type[form_type] = forms_by_tag
			seen_tags_per_form_type[form_type] = seen_tags
			combined_tags_to_tag_lists_per_form_type[form_type] = combined_tags_to_tag_lists
			for _, formobj in ipairs(seen_forms) do
				if not cur_tag_list and form_equals_pagename(formobj, pagename, m_data, lang) then
					cur_tag_list = formobj.tag or {}
					cur_combined_tag = tag_list_to_combined_tag(cur_tag_list)
					cur_type = cur_type or form_type.key
				end
			end
		end
	end

	-- Error if we couldn't locate the pagename among the forms for the current number. This only happens if the
	-- number if given explicitly in 2=.

	if not cur_type and mw.title.getCurrentTitle().nsText ~= "Template" then
		error("The current page name '" .. pagename .. "' does not match any of the numbers listed in [[" ..
			module_name .. "]] for " .. cur_num .. ". Check the data module or the spelling of the page.")
	end

	-- Now, format all the forms for all form types for the current number.

	local function sort_combined_tags(combined_tags, seen_tags, combined_tags_to_tag_lists)
		-- cur_tag_list should normally never be nil, but can be so in template space
		local cur_tag_set = list_to_set(cur_tag_list or {})
		local tags_to_order = {}
		for i, tag in ipairs(seen_tags) do
			tags_to_order[tag] = i
		end
		local function compare_tags(tag1, tag2)
			-- See long comment above.
			-- First compare by number of tags in common with the current tag list.
			local tag_list1 = combined_tags_to_tag_lists[tag1]
			local tag_list2 = combined_tags_to_tag_lists[tag2]
			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 n_common1, n_common2 = set_size(common1), set_size(common2)
			if n_common1 ~= n_common2 then
				return n_common1 > n_common2 -- larger overlap with current tag list first
			end
			-- When overlap ties, shorter tag lists first (untagged default before explicit <tag:...> rows).
			if #tag_list1 ~= #tag_list2 then
				return #tag_list1 < #tag_list2
			end
			-- Finally, compare by the original ordering in the number data, but if a tag is the same as the current
			-- tag, put it first, and if somehow we encounter a tag that's not in the original ordering, put it last.
			local index1 = tag1 == cur_combined_tag and 0 or tags_to_order[tag1] or #seen_tags + 1
			local index2 = tag2 == cur_combined_tag and 0 or tags_to_order[tag2] or #seen_tags + 1
			return index1 < index2
		end
		sort(combined_tags, compare_tags)
	end

	for _, form_type in ipairs(form_types) do
		local forms_by_tag = forms_by_tag_per_form_type[form_type]
		local seen_tags = seen_tags_per_form_type[form_type]
		local combined_tags_to_tag_lists = combined_tags_to_tag_lists_per_form_type[form_type]
		if forms_by_tag then
			local function insert_forms_by_tag(tag)
				local formatted_tag_forms = {}

				local pagename_among_forms = false
				for _, formobj in ipairs(forms_by_tag[tag]) do
					insert(formatted_tag_forms, export.format_formobj(formobj, m_data, lang))
					if form_equals_pagename(formobj, pagename, m_data, lang) then
						pagename_among_forms = true
					end
				end

				if tag ~= "" then
					local tag_list = combined_tags_to_tag_lists[tag]
					tag = concat(tag_list, " / ")
				end
				local displayed_number_type = export.display_number_type(form_type) .. (tag == "" and "" or (" (%s)"):format(tag))
				if pagename_among_forms then
					displayed_number_type = "'''" .. displayed_number_type .. "'''"
				end

				insert(formatted_forms, " &nbsp;&nbsp;&nbsp; ''" .. displayed_number_type .. "'': " ..
					concat(formatted_tag_forms, ", "))
			end

			sort_combined_tags(seen_tags, seen_tags, combined_tags_to_tag_lists)
			for _, tag in ipairs(seen_tags) do
				insert_forms_by_tag(tag)
			end
		end
	end

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

	local numeral
	if m_data.numeral_config then
		numeral = export.generate_non_arabic_numeral(m_data.numeral_config, cur_num)
	elseif cur_data["numeral"] then
		numeral = number_system.normalize_data_key(cur_data["numeral"])
	end

	if numeral then
		cur_display = full_link{lang = lang, alt = numeral, tr = "-"} .. "<br/><span style=\"font-size: smaller;\">" .. cur_display .. "</span>"
	end

	--------------------- Determine next/prev, next/prev outer, and upper/lower numbers. ----------------------

	-- We have three series of numbers to determine:
	--
	-- 1. The next/previous numbers, which are always those in the sorted series of available numbers unless overridden
	--    by `next`/`prev` specs in an individual number.
	-- 2. The next/previous outer numbers, which are displayed to the outside of the next/previous numbers. These can
	--    be overridden for an individual number using `next_outer`/`prev_outer`. Otherwise, we try according to an
	--    algorithm described below in the code for computing the outer numbers.
	-- 3. The upper/lower numbers, which are displayed above or below the central number box. These can be overridden
	--    for an individual number using `upper`/`lower`. These are always 10x greater or less than the number in
	--    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, number_system, lookup_data)
	local next_data = next_num and lookup_data(next_num, "next")
	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 =
		number_system.derive_related_numbers(cur_num, cur_data, next_num, prev_num, lookup_data)

	-- For a number `num` (an "adjacent" number to the current number, i.e. either next, previous, next/previous outer,
	-- or upper/lower) with corresponding entry data `num_data`, display link(s) to the form(s) for this number that
	-- are associated with the current type and tag. If there is a single form to be linked to, the form is linked
	-- using the number itself as the display text; otherwise, the multiple forms are linked with superscripted [a],
	-- [b], etc. and the number it displayed adjacent to the links. In either case, beside the number there may be an
	-- arrow. If `arrow` == "rarrow", the format is like this:
	--		if multiple entries:
	--			<numeral> → <sup>[a], [b], ...</sup>
	--		else
	--			<numeral> →
	-- If `arrow` == "larrow", the format is like this:
	--		if multiple entries:
	--			<sup>[a], [b], ...</sup> ← <numeral>
	--		else
	--			← <numeral>
	-- Otherwise, the format is like this:
	--		if multiple entries:
	--			<numeral><sup>[a], [b], ...</sup>
	--		else
	--			<numeral>
	--
	-- Returns nil if `num_data` is nil or there is no entry in `num_data` for the current number type.
	--
	-- For the handling of tags in this function, see the "LONG COMMENT EXPLAINING TAG HANDLING" above.
	local function display_adjacent_number_links(num, num_data, arrow)
		if not num_data then
			return nil
		end
		local forms = num_data[cur_type]
		if not forms then
			return nil
		elseif type(forms) ~= "table" then
			forms = {forms}
		end

		local seen_forms, forms_by_tag = export.group_numeral_forms_by_tag(forms)

		-- FIXME: `cur_tag` is not defined. This seems to have been missed when multiple tag handling was added in [[Special:Diff/68978046]].
		local forms_to_display
		if cur_tag and forms_by_tag[cur_tag] then
			forms_to_display = forms_by_tag[cur_tag]
		else
			forms_to_display = seen_forms
		end

		for i, form_to_display in ipairs(forms_to_display) do
			forms_to_display[i] = form_to_display.link or maybe_unaffix(m_data,
				form_to_stripped_form(form_to_display.form, lang))
		end

		local seen_pagenames = {}
		local pagenames_to_display = {}
		for _, form in ipairs(forms_to_display) do
			if not seen_pagenames[form] then
				insert(pagenames_to_display, form)
				seen_pagenames[form] = true
			end
		end

		if #pagenames_to_display == 0 then
			return nil
		end

		num = number_system.format_key_for_display(num)
		local num_arrow =
			arrow == "rarrow" and num .. "&nbsp;&nbsp;→&nbsp;" or
			arrow == "larrow" and "&nbsp;←&nbsp;&nbsp;" .. num or
			num
		if #pagenames_to_display > 1 then
			local a = ("a"):byte()
			local links = {}
			for i, term in ipairs(pagenames_to_display) do
				links[i] = m_links.language_link{lang = lang, term = term, alt = "[" .. char(a + i - 1) .. "]"}
			end
			links = "<sup>" .. concat(links, ", ") .. "</sup>"
			return arrow == "larrow" and links .. num_arrow or num_arrow .. links
		else
			return m_links.language_link {
				lang = lang,
				term = pagenames_to_display[1],
				alt = num_arrow,
			}
		end
	end

	-- Display links to previous/next numbers
	local prev_display = display_adjacent_number_links(prev_num, prev_data, "larrow") or ""
	local next_display = display_adjacent_number_links(next_num, next_data, "rarrow") or ""

	-- Display links to previous/next outer numbers
	local prev_outer_display = display_adjacent_number_links(prev_outer_num, prev_outer_data, "larrow")
	local next_outer_display = display_adjacent_number_links(next_outer_num, next_outer_data, "rarrow")

	-- Display links to upper/lower numbers
	local upper_display = display_adjacent_number_links(upper_num, upper_data)
	local lower_display = display_adjacent_number_links(lower_num, lower_data)

	local canonical_name = lang:getCanonicalName()
	local title = canonical_name .. " numerals"

	local function format_cell(contents, class_name, colspan, bold)
		class_name = class_name and (" " .. class_name) or ""
		colspan = colspan and ('colspan="%s" '):format(colspan) or ""
		bold = bold and "!" or "|"
		return ('%s %sclass="table-cell %s | %s\n'):format(bold, colspan, class_name, contents)
	end

	local has_outer_display = not not (prev_outer_display or next_outer_display)
	local function format_upper_lower_display_row(display)
		local blank_cell
		if has_outer_display then
			blank_cell = '| colspan="2" |\n'
		else
			blank_cell = "|\n"
		end
		local parts = {'|- class="adjacent-panel"\n'}
		insert(parts, blank_cell)
		insert(parts, format_cell(display, "adjacent-number"))
		insert(parts, blank_cell)
		return concat(parts)
	end

	upper_display = upper_display and format_upper_lower_display_row(upper_display) or ""
	lower_display = lower_display and format_upper_lower_display_row(lower_display) or ""

	local function format_display_cell(display)
		return format_cell(display, "adjacent-number")
	end

	prev_display = format_display_cell(prev_display)
	next_display = format_display_cell(next_display)
	prev_outer_display = has_outer_display and format_display_cell(prev_outer_display or "") or ""
	next_outer_display = has_outer_display and format_display_cell(next_outer_display or "") or ""
	cur_display = format_cell(cur_display, "current-number", nil, "bold")

	local forms_display = ('| colspan="%s" style="text-align: center;" | %s\n'):format(
		has_outer_display and 5 or 3, concat(formatted_forms, "<br/>"))

	local footer_display
	if cur_data.wplink then
		local footer =
			"[[w:" .. lang:getCode() .. ":|" .. lang:getCanonicalName() .. " Wikipedia]] article on " ..
			m_links.full_link{lang = lang, term = "w:" .. lang:getCode() .. ":" .. cur_data.wplink,
			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)
	else
		footer_display = ""
	end

	local edit_link = ' <sup>(<span class="plainlinks">[' ..
		tostring(mw.uri.fullUrl(module_name, { action = "edit" })) ..
		" edit]</span>)</sup>"

	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" ..
	upper_display .. '|- style="text-align: center;"\n' ..
	prev_outer_display .. prev_display .. cur_display .. next_display .. next_outer_display .. "|-\n" ..
	lower_display .. "|-\n" ..
	forms_display .. footer_display .. "|}" ..
	require("Module:TemplateStyles")("Template:number box/styles.css")
end


function export.show_box_manual(frame)
	local m_links = require("Module:links")
	local num_type = frame.args["type"]

	local args = require("Module:parameters").process(frame:getParent().args, {
		[1] = {required = true, type = "language", default = "und"},
		sc = {type = "script"},
		headlink = true,
		wplink = true,
		alt = true,
		tr = true,
		[2] = true, -- prev_symbol
		[3] = true, -- cur_symbol
		[4] = true, -- next_symbol
		[5] = true, -- prev_term
		[6] = true, -- next_term
		card = true, cardalt = true, cardtr = true,
		ord = true, ordalt = true, ordtr = true,
		adv = true, advalt = true, advtr = true,
		mult = true, multalt = true, multtr = true,
		dis = true, disalt = true, distr = true,
		coll = true, collalt = true, colltr = true,
		frac = true, fracalt = true, fractr = true,
		opt = true, optx = true, optxalt = true, optxtr = true,
		opt2 = true, opt2x = true, opt2xalt = true, opt2xtr = true,
	})

	local lang = args[1]
	local sc = args.sc
	local headlink = args.headlink
	local wplink = args.wplink
	local alt = args.alt
	local tr = args.tr

	local prev_symbol = args[2]
	local cur_symbol = args[3]
	local next_symbol = args[4]

	local prev_term = args[5]
	local next_term = args[6]

	local cardinal_term = args.card; local cardinal_alt = args.cardalt; local cardinal_tr = args.cardtr

	local ordinal_term = args.ord; local ordinal_alt = args.ordalt; local ordinal_tr = args.ordtr

	local adverbial_term = args.adv; local adverbial_alt = args.advalt; local adverbial_tr = args.advtr

	local multiplier_term = args.mult; local multiplier_alt = args.multalt; local multiplier_tr = args.multtr

	local distributive_term = args.dis; local distributive_alt = args.disalt; local distributive_tr = args.distr

	local collective_term = args.coll; local collective_alt = args.collalt; local collective_tr = args.colltr

	local fractional_term = args.frac; local fractional_alt = args.fracalt; local fractional_tr = args.fractr

	local optional1_title = args.opt
	local optional1_term = args.optx; local optional1_alt = args.optxalt; local optional1_tr = args.optxtr

	local optional2_title = args.opt2
	local optional2_term = args.opt2x; local optional2_alt = args.opt2xalt; local optional2_tr = args.opt2xtr

	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"
	
	-- Commenting out this line prevents passing redundant alts to full_link;
	-- however, there may have been a purpose to it.
	-- alt = alt or (is_reconstructed and "*" or "") .. subpage

	if num_type == "cardinal" then
		cardinal_term = cardinal_term or (is_reconstructed and "*" or "") .. subpage
		cardinal_alt = cardinal_alt or alt
		cardinal_tr = cardinal_tr or tr
	elseif num_type == "ordinal" then
		ordinal_term = ordinal_term or (is_reconstructed and "*" or "") .. subpage
		ordinal_alt = ordinal_alt or alt
		ordinal_tr = ordinal_tr or tr
	end

	local header = lang:getCanonicalName() .. " " .. num_type .. " numbers"

	if headlink then
		header = "[[" .. headlink .. "|" .. header .. "]]"
	end

	local previous = ""

	if prev_term or prev_symbol then
		previous = m_links.full_link{lang = lang, sc = sc, term = prev_term, alt = "&nbsp;&lt;&nbsp;&nbsp;" .. prev_symbol, tr = "-", no_alt_ast = true}
	end

	local current = m_links.full_link{lang = lang, sc = sc, alt = cur_symbol, tr = "-", no_alt_ast = true}

	local next = ""

	if next_term or next_symbol then
		next = m_links.full_link{lang = lang, sc = sc, term = next_term, alt = next_symbol .. "&nbsp;&nbsp;&gt;&nbsp;", tr = "-", no_alt_ast = true}
	end

	local forms = {}

	if cardinal_term then
		insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:cardinal number|Cardinal]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = cardinal_term, alt = cardinal_alt, tr = cardinal_tr})
	end

	if ordinal_term then
		insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:ordinal number|Ordinal]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = ordinal_term, alt = ordinal_alt, tr = ordinal_tr})
	end

	if adverbial_term then
		insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:adverbial number|Adverbial]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = adverbial_term, alt = adverbial_alt, tr = adverbial_tr})
	end

	if multiplier_term then
		insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:multiplier|Multiplier]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = multiplier_term, alt = multiplier_alt, tr = multiplier_tr})
	end

	if distributive_term then
		insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:distributive number|Distributive]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = distributive_term, alt = distributive_alt, tr = distributive_tr})
	end

	if collective_term then
		insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:collective number|Collective]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = collective_term, alt = collective_alt, tr = collective_tr})
	end

	if fractional_term then
		insert(forms, " &nbsp;&nbsp;&nbsp; ''[[wikt:fractional|Fractional]]'' : " .. m_links.full_link{lang = lang, sc = sc, term = fractional_term, alt = fractional_alt, tr = fractional_tr})
	end

	if optional1_title then
		insert(forms, " &nbsp;&nbsp;&nbsp; ''" .. optional1_title .. "'' : " .. m_links.full_link{lang = lang, sc = sc, term = optional1_term, alt = optional1_alt, tr = optional1_tr})
	end

	if optional2_title then
		insert(forms, " &nbsp;&nbsp;&nbsp; ''" .. optional2_title .. "'' : " .. m_links.full_link{lang = lang, sc = sc, term = optional2_term, alt = optional2_alt, tr = optional2_tr})
	end

	local footer = ""

	if wplink then
		footer =
			"[[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}
	end

	return [=[{| class="floatright number-box" cellpadding="5" cellspacing="0" rules="all"
|+ ''']=] .. header .. [=['''
|-
| class="adjacent-slot" | ]=] .. previous .. [=[

! class="current-slot" | ]=] .. current .. [=[

| class="adjacent-slot" | ]=] .. next .. [=[

|-
| colspan="3" class="form-slot" | ]=] .. concat(forms, "<br/>") .. [=[

|-
| colspan="3" class="footer-slot" | ]=] .. footer .. [=[

|}]=] .. require("Module:TemplateStyles")("Template:number box/styles.css")
end

return export