Module:number list: Difference between revisions
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
local m_links = require("Module:links") | local m_links = require("Module:links") | ||
local m_str_utils = require("Module:string utilities") | local m_str_utils = require("Module:string utilities") | ||
| Line 10: | Line 8: | ||
local list_to_set = require("Module:table").listToSet | local list_to_set = require("Module:table").listToSet | ||
local sort = table.sort | local sort = table.sort | ||
local split = m_str_utils.split | |||
local u = m_str_utils.char | local u = m_str_utils.char | ||
local unpack = unpack or table.unpack -- Lua 5.2 compatibility | local unpack = unpack or table.unpack -- Lua 5.2 compatibility | ||
| Line 15: | Line 14: | ||
local export = {} | local export = {} | ||
local decimal_strategy | |||
--[=[ | --[=[ | ||
| Line 34: | Line 34: | ||
local default_form_types = { | local default_form_types = { | ||
{key = "cardinal", display = "[[ | {key = "cardinal", display = "[[wikt:cardinal number|Cardinal]]"}, | ||
{key = "ordinal", display = "[[ | {key = "ordinal", display = "[[wikt:ordinal number|Ordinal]]"}, | ||
{key = "ordinal_abbr", display = "[[ | {key = "ordinal_abbr", display = "[[wikt:ordinal number|Ordinal]] [[wikt:abbreviation|abbreviation]]"}, | ||
{key = "adverbial", display = "[[ | {key = "adverbial", display = "[[wikt:adverbial number|Adverbial]]"}, | ||
{key = "multiplier", display = "[[ | {key = "multiplier", display = "[[wikt:multiplier|Multiplier]]"}, | ||
{key = "distributive", display = "[[ | {key = "distributive", display = "[[wikt:distributive number|Distributive]]"}, | ||
{key = "collective", display = "[[ | {key = "collective", display = "[[wikt:collective number|Collective]]"}, | ||
{key = "fractional", display = "[[ | {key = "fractional", display = "[[wikt:fractional|Fractional]]"}, | ||
} | } | ||
| Line 55: | Line 55: | ||
lower = true, | lower = true, | ||
} | } | ||
local function track(page) | |||
require("Module:debug/track")("number list/" .. page) | |||
return true | |||
end | |||
--[=[ | --[=[ | ||
| Line 91: | Line 96: | ||
end | end | ||
return intersection | return intersection | ||
end | |||
-- Count keys in a set table (never use `#` on these; it is not the set cardinality). | |||
local function set_size(set) | |||
local n = 0 | |||
for _ in pairs(set) do | |||
n = n + 1 | |||
end | |||
return n | |||
end | end | ||
| Line 109: | Line 123: | ||
-- Parse a form with modifiers such as 'vuitanta-vuit<tag:Central>' or 'سیزده<tr:sizdah>' | -- Parse a form with modifiers such as 'vuitanta-vuit<tag:Central>' or 'سیزده<tr:sizdah>' | ||
-- or 'سیزده<tr:sizdah><tag:Iranian>' into its component parts. Return a form object, i.e. an object with fields | -- or 'سیزده<tr:sizdah><tag:Iranian>' into its component parts. Return a form object, i.e. an object with fields | ||
-- `form` for the form, and `tr`, `tag`, `q`, `qq` or `link` for the modifiers. The `tag` field is a tag list | -- `form` for the form, and `tr`, `tag`, `q`, `qq`, `g` or `link` for the modifiers. The `tag` field is a tag list | ||
-- (see above). | -- (see above). | ||
function export.parse_form_and_modifiers(form_with_modifiers) | function export.parse_form_and_modifiers(form_with_modifiers) | ||
| Line 130: | Line 144: | ||
retval.tag = {content} | retval.tag = {content} | ||
end | end | ||
elseif prefix == "q" or prefix == "qq" or prefix == "tr" or prefix == "link" or prefix == "id" or prefix == "alt" then | elseif prefix == "q" or prefix == "qq" or prefix == "tr" or prefix == "link" or prefix == "id" or prefix == "g" or prefix == "alt" then | ||
if retval[prefix] then | if retval[prefix] then | ||
error(("Duplicate modifier '%s' in data module form, already saw value '%s': %s"):format(prefix, | error(("Duplicate modifier '%s' in data module form, already saw value '%s': %s"):format(prefix, | ||
| Line 170: | Line 184: | ||
function export.numbers_greater_than(a, b) | function export.numbers_greater_than(a, b) | ||
return export.numbers_less_than(b, a) | return export.numbers_less_than(b, a) | ||
end | |||
local POSITIONAL_DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" | |||
local MAX_SAFE_INTEGER = 9007199254740991 | |||
local function get_digit_maps(base, digit_alphabet) | |||
digit_alphabet = digit_alphabet or POSITIONAL_DIGITS | |||
if #digit_alphabet < base then | |||
error(("Number system base %s exceeds available digits in digit alphabet"):format(base)) | |||
end | |||
local digit_to_value = {} | |||
local value_to_digit = {} | |||
for i = 1, base do | |||
local digit = digit_alphabet:sub(i, i) | |||
digit_to_value[digit] = i - 1 | |||
value_to_digit[i - 1] = digit | |||
end | |||
return digit_to_value, value_to_digit | |||
end | |||
local function decimal_to_base(num, base, value_to_digit) | |||
if num == 0 then | |||
return value_to_digit[0] | |||
end | |||
local parts = {} | |||
while num > 0 do | |||
insert(parts, 1, value_to_digit[num % base]) | |||
num = math.floor(num / base) | |||
end | |||
return concat(parts) | |||
end | |||
local function parse_positional_to_number(key, base, digit_to_value) | |||
local n = 0 | |||
for i = 1, #key do | |||
local value = digit_to_value[key:sub(i, i)] | |||
if not value then | |||
return nil | |||
end | |||
n = n * base + value | |||
if n > MAX_SAFE_INTEGER then | |||
return nil | |||
end | |||
end | |||
return n | |||
end | |||
local function safe_integer_power(base, exp) | |||
local result = 1 | |||
for _ = 1, exp do | |||
if result > MAX_SAFE_INTEGER / base then | |||
return nil | |||
end | |||
result = result * base | |||
end | |||
return result | |||
end | |||
local function normalize_positional_key(raw_number, opts) | |||
local base = opts.base | |||
local case_insensitive = opts.case_insensitive | |||
local digit_to_value = opts.digit_to_value | |||
local value_to_digit = opts.value_to_digit | |||
local strip_separator = opts.strip_separator | |||
local interpret_plain_decimal = opts.interpret_plain_decimal | |||
local zero_digit = opts.zero_digit | |||
local key | |||
if type(raw_number) == "number" then | |||
if raw_number < 0 or raw_number % 1 ~= 0 then | |||
error(("Non-negative integer expected for positional number system, got '%s'"):format(raw_number)) | |||
end | |||
key = decimal_to_base(raw_number, base, value_to_digit) | |||
else | |||
key = tostring(raw_number) | |||
if strip_separator and strip_separator ~= "" then | |||
key = gsub(key, strip_separator, "") | |||
end | |||
-- For compatibility with existing data/modules, plain decimal-digit *input* can be interpreted | |||
-- as decimal and then converted into the configured positional key space. | |||
if interpret_plain_decimal and key:find("^%d+$") then | |||
key = decimal_to_base(tonumber(key), base, value_to_digit) | |||
end | |||
end | |||
if case_insensitive then | |||
key = upper(key) | |||
end | |||
if key == "" then | |||
return zero_digit | |||
end | |||
for i = 1, #key do | |||
local digit = key:sub(i, i) | |||
if not digit_to_value[digit] then | |||
error(("Extraneous characters in number: '%s'"):format(key)) | |||
end | |||
end | |||
key = key:gsub("^" .. zero_digit .. "+", "") | |||
return key == "" and zero_digit or key | |||
end | end | ||
| Line 191: | Line 307: | ||
-- Convert the given number form (taken from the data for `lang`, after parsing the form for modifiers and stripping | -- Convert the given number form (taken from the data for `lang`, after parsing the form for modifiers and stripping | ||
-- the modifiers) to | -- the modifiers) to the stripped-text version of the form. The form may have links and/or accent/length marks that need | ||
local function | -- to be stripped. | ||
return | local function form_to_stripped_form(form, lang) | ||
return lang:stripDiacritics(m_links.remove_links(form)) | |||
end | end | ||
| Line 205: | Line 322: | ||
return true | return true | ||
end | end | ||
local | local stripped_form = form_to_stripped_form(formobj.form, lang) | ||
return | if stripped_form == pagename or maybe_unaffix(m_data, stripped_form) == pagename then | ||
return true | |||
end | |||
if formobj.alt then | |||
local stripped_alt = form_to_stripped_form(formobj.alt, lang) | |||
if stripped_alt == pagename or maybe_unaffix(m_data, stripped_alt) == pagename then | |||
return true | |||
end | |||
end | |||
return false | |||
end | end | ||
-- Given the data for a language and a number (which should be in string representation), find the next and previous | -- Given the data for a language and a number (which should be in string representation), find the next and previous | ||
-- numbers to display (in string representation). | -- numbers to display (in string representation). | ||
local function get_next_and_prev_keys(m_data, numstr) | local function get_next_and_prev_keys(m_data, numstr, strategy, lookup_data) | ||
local numdata = | local numdata = lookup_data(numstr) | ||
if not numdata then | if not numdata then | ||
return nil, nil | return nil, nil | ||
| Line 221: | Line 347: | ||
-- Find the next/previous numbers by sorting all the keys and locating the number in question among them. | -- Find the next/previous numbers by sorting all the keys and locating the number in question among them. | ||
local sorted_list = {} | local sorted_list = {} | ||
local seen = {} | |||
local index = 1 | local index = 1 | ||
for key, _ in pairs(m_data.numbers) do | for key, _ in pairs(m_data.numbers) do | ||
sorted_list[index] = | 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 | end | ||
sort(sorted_list, | sort(sorted_list, strategy.compare_keys) | ||
-- We could binary search to save time, but given that we already sort, which is supra-linear, it won't | -- We could binary search to save time, but given that we already sort, which is supra-linear, it won't | ||
-- matter to search linearly. | -- matter to search linearly. | ||
for i, key in ipairs(sorted_list) do | for i, key in ipairs(sorted_list) do | ||
if | if key == numstr then | ||
nextnum = nextnum or sorted_list[i + 1] | nextnum = nextnum or sorted_list[i + 1] | ||
prevnum = prevnum or sorted_list[i - 1] | prevnum = prevnum or sorted_list[i - 1] | ||
| Line 241: | Line 372: | ||
if nextnum then | if nextnum then | ||
nextnum = | nextnum = strategy.normalize_data_key(nextnum) | ||
end | end | ||
if prevnum then | if prevnum then | ||
prevnum = | prevnum = strategy.normalize_data_key(prevnum) | ||
end | end | ||
| Line 433: | Line 564: | ||
return fixed .. " (" .. scientific .. ")" | return fixed .. " (" .. scientific .. ")" | ||
end | end | ||
end | |||
local function derive_related_numbers_decimal(cur_num, cur_data, next_num, prev_num, lookup_data) | |||
local k, m | |||
if cur_num == "0" then | |||
k = 0 | |||
m = 1 | |||
else | |||
local kstr, mstr = cur_num:match("^([0-9]*[1-9])(0*)$") | |||
if not kstr then | |||
error("Internal error: Unable to match number '" .. cur_num .. "'") | |||
elseif #kstr > 15 then | |||
error("Can't handle number with more than 15 digits before the trailing zeros: '" .. cur_num .. "'") | |||
end | |||
k = tonumber(kstr) | |||
m = #mstr | |||
end | |||
local function make_greater_power_of_ten(power) | |||
return cur_num .. ("0"):rep(power) | |||
end | |||
local function make_lesser_power_of_ten(power) | |||
local desired_zeros = m - power | |||
if desired_zeros < 0 then | |||
return nil | |||
end | |||
return k .. ("0"):rep(desired_zeros) | |||
end | |||
local next_outer_data, prev_outer_data | |||
local next_outer_num, prev_outer_num = cur_data.next_outer, cur_data.prev_outer | |||
local power_of_10_sequence = { 1, 3, 2, 6 } | |||
if next_outer_num then | |||
next_outer_data = lookup_data(next_outer_num, "next outer") | |||
else | |||
local function try(num) | |||
local data = (not next_num or export.numbers_greater_than(num, next_num)) and lookup_data(num) or nil | |||
if data then | |||
next_outer_num = num | |||
next_outer_data = data | |||
end | |||
return data | |||
end | |||
if not try((k + 1) .. ("0"):rep(m)) and k == 1 then | |||
for _, power_of_10 in ipairs(power_of_10_sequence) do | |||
if try(make_greater_power_of_ten(power_of_10)) then | |||
break | |||
end | |||
end | |||
end | |||
end | |||
if prev_outer_num then | |||
prev_outer_data = lookup_data(prev_outer_num, "previous outer") | |||
else | |||
local function try(num) | |||
local data = (not prev_num or export.numbers_less_than(num, prev_num)) and lookup_data(num) or nil | |||
if data then | |||
prev_outer_num = num | |||
prev_outer_data = data | |||
end | |||
return data | |||
end | |||
if not (k == 0 or m == 0) then | |||
local num_to_try | |||
if k == 1 then | |||
num_to_try = "9" .. ("0"):rep(m - 1) | |||
else | |||
num_to_try = (k - 1) .. ("0"):rep(m) | |||
end | |||
if not try(num_to_try) and k == 1 then | |||
for _, power_of_10 in ipairs(power_of_10_sequence) do | |||
local power_num_to_try = make_lesser_power_of_ten(power_of_10) | |||
if power_num_to_try and try(power_num_to_try) then | |||
break | |||
end | |||
end | |||
end | |||
end | |||
end | |||
local upper_data, lower_data | |||
local upper_num, lower_num = cur_data.upper, cur_data.lower | |||
if upper_num then | |||
upper_data = lookup_data(upper_num, "upper") | |||
else | |||
upper_num = make_greater_power_of_ten(1) | |||
if upper_num == next_num or cur_num == "0" then | |||
upper_num = nil | |||
else | |||
upper_data = lookup_data(upper_num) | |||
end | |||
end | |||
if lower_num then | |||
lower_data = lookup_data(lower_num, "lower") | |||
elseif not (k == 0 or m == 0) then | |||
lower_num = make_lesser_power_of_ten(1) | |||
if lower_num == prev_num then | |||
lower_num = nil | |||
else | |||
lower_data = lookup_data(lower_num) | |||
end | |||
end | |||
return next_outer_num, next_outer_data, prev_outer_num, prev_outer_data, upper_num, upper_data, lower_num, lower_data | |||
end | |||
local function derive_related_numbers_positional(strategy, cur_num, cur_data, next_num, prev_num, lookup_data) | |||
local zero_digit = strategy.zero_digit | |||
local base = strategy.base | |||
local power_base = strategy.power_base | |||
local value_to_digit = strategy.value_to_digit | |||
local digit_to_value = strategy.digit_to_value | |||
local power_sequence = strategy.power_sequence | |||
local significant = cur_num:gsub(zero_digit .. "+$", "") | |||
local m = #cur_num - #significant | |||
local kstr = significant == "" and zero_digit or significant | |||
local k_value = parse_positional_to_number(kstr, base, digit_to_value) | |||
local next_outer_data, prev_outer_data | |||
local next_outer_num, prev_outer_num = cur_data.next_outer, cur_data.prev_outer | |||
local function value_to_key(value) | |||
return decimal_to_base(value, base, value_to_digit) | |||
end | |||
local function make_greater_power(power) | |||
if not k_value then | |||
return nil | |||
end | |||
local factor = safe_integer_power(power_base, power) | |||
if not factor then | |||
return nil | |||
end | |||
local cur_value = parse_positional_to_number(cur_num, base, digit_to_value) | |||
if not cur_value or cur_value > MAX_SAFE_INTEGER / factor then | |||
return nil | |||
end | |||
return value_to_key(cur_value * factor) | |||
end | |||
local function make_lesser_power(power) | |||
local factor = safe_integer_power(power_base, power) | |||
if not factor then | |||
return nil | |||
end | |||
local cur_value = parse_positional_to_number(cur_num, base, digit_to_value) | |||
if not cur_value or cur_value % factor ~= 0 then | |||
return nil | |||
end | |||
return value_to_key(cur_value / factor) | |||
end | |||
if next_outer_num then | |||
next_outer_data = lookup_data(next_outer_num, "next outer") | |||
else | |||
local function try(num) | |||
local data = (not next_num or strategy.compare_keys(next_num, num)) and lookup_data(num) or nil | |||
if data then | |||
next_outer_num = num | |||
next_outer_data = data | |||
end | |||
return data | |||
end | |||
if k_value then | |||
if not try(decimal_to_base(k_value + 1, base, value_to_digit) .. zero_digit:rep(m)) and k_value == 1 then | |||
for _, power in ipairs(power_sequence) do | |||
if try(make_greater_power(power)) then | |||
break | |||
end | |||
end | |||
end | |||
end | |||
end | |||
if prev_outer_num then | |||
prev_outer_data = lookup_data(prev_outer_num, "previous outer") | |||
else | |||
local function try(num) | |||
local data = (not prev_num or strategy.compare_keys(num, prev_num)) and lookup_data(num) or nil | |||
if data then | |||
prev_outer_num = num | |||
prev_outer_data = data | |||
end | |||
return data | |||
end | |||
if not (cur_num == zero_digit or m == 0) and k_value then | |||
local num_to_try | |||
if k_value == 1 then | |||
num_to_try = value_to_digit[base - 1] .. zero_digit:rep(m - 1) | |||
else | |||
num_to_try = decimal_to_base(k_value - 1, base, value_to_digit) .. zero_digit:rep(m) | |||
end | |||
if not try(num_to_try) and k_value == 1 then | |||
for _, power in ipairs(power_sequence) do | |||
local power_num_to_try = make_lesser_power(power) | |||
if power_num_to_try and try(power_num_to_try) then | |||
break | |||
end | |||
end | |||
end | |||
end | |||
end | |||
local upper_data, lower_data | |||
local upper_num, lower_num = cur_data.upper, cur_data.lower | |||
if upper_num then | |||
upper_data = lookup_data(upper_num, "upper") | |||
else | |||
upper_num = make_greater_power(1) | |||
if upper_num == next_num or cur_num == zero_digit then | |||
upper_num = nil | |||
else | |||
upper_data = lookup_data(upper_num) | |||
end | |||
end | |||
if lower_num then | |||
lower_data = lookup_data(lower_num, "lower") | |||
elseif not (cur_num == zero_digit or m == 0) then | |||
lower_num = make_lesser_power(1) | |||
if lower_num == prev_num then | |||
lower_num = nil | |||
else | |||
lower_data = lookup_data(lower_num) | |||
end | |||
end | |||
return next_outer_num, next_outer_data, prev_outer_num, prev_outer_data, upper_num, upper_data, lower_num, lower_data | |||
end | |||
local function create_positional_strategy(config) | |||
local base = config.base | |||
local digit_alphabet = config.digit_alphabet or POSITIONAL_DIGITS | |||
local case_insensitive = config.case_insensitive | |||
if case_insensitive == nil then | |||
case_insensitive = base <= 36 | |||
end | |||
local strip_separator = config.strip_separator or "," | |||
local digit_to_value, value_to_digit = get_digit_maps(base, digit_alphabet) | |||
local zero_digit = value_to_digit[0] | |||
local display_separator = config.display_separator | |||
local display_group = config.display_group | |||
local display_group_start = config.display_group_start | |||
local display_indic = config.display_indic | |||
local display_keys_as_decimal = config.display_keys_as_decimal | |||
if display_keys_as_decimal == nil then | |||
display_keys_as_decimal = true | |||
end | |||
local power_sequence = config.power_sequence or {1, 3, 2, 6} | |||
local power_base = config.power_base or 10 | |||
local strategy = { | |||
id = config.id or ("base" .. base), | |||
base = base, | |||
power_base = power_base, | |||
digit_to_value = digit_to_value, | |||
value_to_digit = value_to_digit, | |||
zero_digit = zero_digit, | |||
power_sequence = power_sequence, | |||
} | |||
function strategy.normalize_data_key(raw_number) | |||
return normalize_positional_key(raw_number, { | |||
base = base, | |||
case_insensitive = case_insensitive, | |||
digit_to_value = digit_to_value, | |||
value_to_digit = value_to_digit, | |||
strip_separator = nil, | |||
interpret_plain_decimal = false, | |||
zero_digit = zero_digit, | |||
}) | |||
end | |||
function strategy.normalize_input(raw_number) | |||
return normalize_positional_key(raw_number, { | |||
base = base, | |||
case_insensitive = case_insensitive, | |||
digit_to_value = digit_to_value, | |||
value_to_digit = value_to_digit, | |||
strip_separator = strip_separator, | |||
interpret_plain_decimal = true, | |||
zero_digit = zero_digit, | |||
}) | |||
end | |||
function strategy.normalize_input_candidates(raw_number) | |||
local key_as_system = normalize_positional_key(raw_number, { | |||
base = base, | |||
case_insensitive = case_insensitive, | |||
digit_to_value = digit_to_value, | |||
value_to_digit = value_to_digit, | |||
strip_separator = strip_separator, | |||
interpret_plain_decimal = false, | |||
zero_digit = zero_digit, | |||
}) | |||
local key_as_decimal = normalize_positional_key(raw_number, { | |||
base = base, | |||
case_insensitive = case_insensitive, | |||
digit_to_value = digit_to_value, | |||
value_to_digit = value_to_digit, | |||
strip_separator = strip_separator, | |||
interpret_plain_decimal = true, | |||
zero_digit = zero_digit, | |||
}) | |||
if key_as_system == key_as_decimal then | |||
return {key_as_system} | |||
end | |||
return {key_as_system, key_as_decimal} | |||
end | |||
function strategy.lookup_data(m_data, key) | |||
key = strategy.normalize_data_key(key) | |||
local direct = m_data.numbers[key] | |||
if direct then | |||
return direct | |||
end | |||
local parsed = parse_positional_to_number(key, base, digit_to_value) | |||
return parsed and m_data.numbers[parsed] or nil | |||
end | |||
function strategy.compare_keys(a, b) | |||
a = strategy.normalize_data_key(a) | |||
b = strategy.normalize_data_key(b) | |||
if #a ~= #b then | |||
return #a < #b | |||
end | |||
return a < b | |||
end | |||
function strategy.format_key_for_display(key) | |||
key = strategy.normalize_data_key(key) | |||
if display_keys_as_decimal then | |||
local decimal_value = parse_positional_to_number(key, base, digit_to_value) | |||
if decimal_value then | |||
return export.format_number_for_display(decimal_value) | |||
end | |||
end | |||
if display_separator then | |||
if display_indic then | |||
return add_separator(key, display_separator, 2, 3) | |||
end | |||
return add_separator(key, display_separator, display_group or 3, display_group_start) | |||
end | |||
return key | |||
end | |||
function strategy.derive_related_numbers(cur_num, cur_data, next_num, prev_num, lookup_data) | |||
return derive_related_numbers_positional(strategy, cur_num, cur_data, next_num, prev_num, lookup_data) | |||
end | |||
return strategy | |||
end | |||
decimal_strategy = { | |||
id = "decimal", | |||
base = 10, | |||
zero_digit = "0", | |||
power_sequence = {1, 3, 2, 6}, | |||
} | |||
function decimal_strategy.normalize_data_key(raw_number) | |||
return export.format_fixed(raw_number) | |||
end | |||
function decimal_strategy.normalize_input(raw_number) | |||
local normalized = tostring(raw_number):gsub(",", "") | |||
if not normalized:find("^%d+$") then | |||
error("Extraneous characters in parameter 2: should be decimal number (integer): '" .. normalized .. "'") | |||
end | |||
return normalized | |||
end | |||
function decimal_strategy.normalize_input_candidates(raw_number) | |||
return {decimal_strategy.normalize_input(raw_number)} | |||
end | |||
function decimal_strategy.lookup_data(m_data, key) | |||
return export.lookup_data(m_data, key) | |||
end | |||
function decimal_strategy.compare_keys(a, b) | |||
return export.numbers_less_than(a, b) | |||
end | |||
function decimal_strategy.format_key_for_display(key) | |||
return export.format_number_for_display(key) | |||
end | |||
function decimal_strategy.derive_related_numbers(cur_num, cur_data, next_num, prev_num, lookup_data) | |||
return derive_related_numbers_decimal(cur_num, cur_data, next_num, prev_num, lookup_data) | |||
end | |||
local function resolve_number_system(m_data) | |||
local config = m_data.number_system | |||
if not config then | |||
return decimal_strategy | |||
end | |||
if type(config) == "string" then | |||
config = {id = config} | |||
end | |||
if config.id == "decimal" then | |||
return decimal_strategy | |||
end | |||
if config.id == "base20" then | |||
config.base = config.base or 20 | |||
elseif config.id == "base60" then | |||
config.base = config.base or 60 | |||
elseif config.id == "positional" then | |||
-- base should be explicitly given or derived below. | |||
elseif config.id == "custom" then | |||
if not (config.module and config.func) then | |||
error("custom number_system requires both module and func") | |||
end | |||
local custom_strategy = require("Module:" .. config.module)[config.func](config) | |||
return custom_strategy | |||
end | |||
config.base = config.base or tonumber(config.id and config.id:match("^base(%d+)$")) | |||
if not config.base then | |||
error("Unsupported number_system id '" .. tostring(config.id) .. "'") | |||
end | |||
if config.base < 2 then | |||
error("number_system base must be >= 2") | |||
end | |||
return create_positional_strategy(config) | |||
end | end | ||
| Line 474: | Line 1,036: | ||
function export.format_formobj(formobj, m_data, lang) | function export.format_formobj(formobj, m_data, lang) | ||
local left_q = formobj.q and require("Module:qualifier").format_qualifier(formobj.q) .. " " or "" | local left_q = formobj.q and require("Module:qualifier").format_qualifier(formobj.q) .. " " or "" | ||
local right_q = formobj.qq and " " .. require("Module:qualifier").format_qualifier(formobj.qq) or "" | local right_q = ((formobj.g and " " .. require("Module:gender and number").format_genders(split(formobj.g, ",")) or "") | ||
.. (formobj.qq and " " .. require("Module:qualifier").format_qualifier(formobj.qq) or "")) | |||
local term = maybe_unaffix(m_data, formobj.form) | local term = maybe_unaffix(m_data, formobj.form) | ||
local alt = | local alt = formobj.alt | ||
if term ~= formobj.form then alt = formobj.form end | if not alt and term ~= formobj.form then | ||
alt = formobj.form | |||
end | |||
return left_q .. m_links.full_link{ | return left_q .. m_links.full_link{ | ||
lang = lang, term = term, alt = alt, tr = formobj.tr, | lang = lang, term = term, alt = alt, tr = formobj.tr, id = formobj.id, | ||
} .. right_q | } .. right_q | ||
end | end | ||
| Line 495: | Line 1,060: | ||
local parent_args = frame:getParent().args | local parent_args = frame:getParent().args | ||
if parent_args.pagename then | |||
track("show-box-pagename") | |||
end | |||
local args = require("Module:parameters").process(parent_args, params, nil, "number list", "show_box") | local args = require("Module:parameters").process(parent_args, params, nil, "number list", "show_box") | ||
| Line 504: | Line 1,072: | ||
local module_name = export.get_data_module_name(langcode) | local module_name = export.get_data_module_name(langcode) | ||
local m_data = require(module_name) | local m_data = require(module_name) | ||
local number_system = resolve_number_system(m_data) | |||
local pagename = args.pagename or (mw.title.getCurrentTitle().nsText == "Reconstruction" and "*" or "") .. mw. | 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 | ||
| Line 532: | Line 1,099: | ||
for _, num_and_type in ipairs(nums_and_types) do | for _, num_and_type in ipairs(nums_and_types) do | ||
local num = num_and_type[1] | local num = num_and_type[1] | ||
num = | num = number_system.normalize_data_key(num) | ||
if cur_num and num ~= cur_num then | if cur_num and num ~= cur_num then | ||
local errparts = {} | local errparts = {} | ||
for _, num_and_type in ipairs(nums_and_types) do | for _, num_and_type in ipairs(nums_and_types) do | ||
local num, typ = unpack(num_and_type) | local num, typ = unpack(num_and_type) | ||
num = number_system.normalize_data_key(num) | |||
insert(errparts, ("%s (%s)"):format(num, typ)) | insert(errparts, ("%s (%s)"):format(num, typ)) | ||
end | end | ||
| Line 547: | Line 1,115: | ||
end | end | ||
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 | end | ||
| Line 555: | Line 1,150: | ||
-- param_for_error is given). | -- param_for_error is given). | ||
local function lookup_data(numstr, param_for_error) | local function lookup_data(numstr, param_for_error) | ||
local retval = | numstr = number_system.normalize_data_key(numstr) | ||
local retval = number_system.lookup_data(m_data, numstr) | |||
if not retval and param_for_error then | if not retval and param_for_error then | ||
error(('The %s number "%s" specified in the "numbers" table entry for "%s" cannot be found in ' | error(('The %s number "%s" specified in the "numbers" table entry for "%s" cannot be found in ' | ||
| Line 667: | Line 1,263: | ||
local common1 = set_intersection(cur_tag_set, list_to_set(tag_list1)) | local common1 = set_intersection(cur_tag_set, list_to_set(tag_list1)) | ||
local common2 = set_intersection(cur_tag_set, list_to_set(tag_list2)) | local common2 = set_intersection(cur_tag_set, list_to_set(tag_list2)) | ||
if | local n_common1, n_common2 = set_size(common1), set_size(common2) | ||
return | if n_common1 ~= n_common2 then | ||
return n_common1 > n_common2 -- larger overlap with current tag list first | |||
end | end | ||
-- | -- When overlap ties, shorter tag lists first (untagged default before explicit <tag:...> rows). | ||
if #tag_list1 ~= #tag_list2 then | if #tag_list1 ~= #tag_list2 then | ||
return #tag_list1 | return #tag_list1 < #tag_list2 | ||
end | end | ||
-- Finally, compare by the original ordering in the number data, but if a tag is the same as the current | -- Finally, compare by the original ordering in the number data, but if a tag is the same as the current | ||
| Line 721: | Line 1,317: | ||
-- Current number in header | -- Current number in header | ||
local cur_display = | local cur_display = number_system.format_key_for_display(cur_num) | ||
local numeral | local numeral | ||
| Line 727: | Line 1,323: | ||
numeral = export.generate_non_arabic_numeral(m_data.numeral_config, cur_num) | numeral = export.generate_non_arabic_numeral(m_data.numeral_config, cur_num) | ||
elseif cur_data["numeral"] then | elseif cur_data["numeral"] then | ||
numeral = | numeral = number_system.normalize_data_key(cur_data["numeral"]) | ||
end | end | ||
| Line 747: | Line 1,343: | ||
-- question, number not considering a number if it's the same as the next/previous number. | -- question, number not considering a number if it's the same as the next/previous number. | ||
local next_num, prev_num = get_next_and_prev_keys(m_data, cur_num) | local next_num, prev_num = get_next_and_prev_keys(m_data, cur_num, number_system, lookup_data) | ||
local next_data = next_num and lookup_data(next_num, "next") | local next_data = next_num and lookup_data(next_num, "next") | ||
local prev_data = prev_num and lookup_data(prev_num, "previous") | local prev_data = prev_num and lookup_data(prev_num, "previous") | ||
local next_outer_num, next_outer_data, prev_outer_num, prev_outer_data, upper_num, upper_data, lower_num, lower_data = | |||
number_system.derive_related_numbers(cur_num, cur_data, next_num, prev_num, lookup_data) | |||
local | |||
-- For a number `num` (an "adjacent" number to the current number, i.e. either next, previous, next/previous outer, | -- For a number `num` (an "adjacent" number to the current number, i.e. either next, previous, next/previous outer, | ||
| Line 941: | Line 1,396: | ||
for i, form_to_display in ipairs(forms_to_display) do | for i, form_to_display in ipairs(forms_to_display) do | ||
forms_to_display[i] = form_to_display.link or maybe_unaffix(m_data, | forms_to_display[i] = form_to_display.link or maybe_unaffix(m_data, | ||
form_to_stripped_form(form_to_display.form, lang)) | |||
end | end | ||
| Line 957: | Line 1,412: | ||
end | end | ||
num = | num = number_system.format_key_for_display(num) | ||
local num_arrow = | local num_arrow = | ||
arrow == "rarrow" and num .. " → " or | arrow == "rarrow" and num .. " → " or | ||
| Line 995: | Line 1,450: | ||
local appendix2 = canonical_name .. " numbers" | local appendix2 = canonical_name .. " numbers" | ||
local appendix | local appendix | ||
local title = appendix2 | local title | ||
if mw.title.new(appendix1, "Appendix"):getContent() then | |||
appendix = appendix1 | |||
elseif mw.title.new(appendix2, "Appendix"):getContent() then | |||
appendix = appendix2 | |||
end | |||
if appendix then | |||
title = "[[wikt:Appendix:" .. appendix .. "|" .. appendix2 .. "]]" | |||
else | |||
title = appendix2 | |||
end | |||
local function format_cell(contents, class_name, colspan, bold) | local function format_cell(contents, class_name, colspan, bold) | ||
| Line 1,038: | Line 1,504: | ||
if cur_data.wplink then | if cur_data.wplink then | ||
local footer = | local footer = | ||
"[[w:" .. lang:getCode() .. ": | "[[w:" .. lang:getCode() .. ":|" .. lang:getCanonicalName() .. " Wikipedia]] article on " .. | ||
m_links.full_link{lang = lang, term = "w:" .. lang:getCode() .. ":" .. cur_data.wplink, | m_links.full_link{lang = lang, term = "w:" .. lang:getCode() .. ":" .. cur_data.wplink, | ||
alt = | alt = number_system.format_key_for_display(cur_num)} | ||
footer_display = '|- style="text-align: center;"\n' .. format_cell(footer, "footer-cell", has_outer_display and 5 or 3) | footer_display = '|- style="text-align: center;"\n' .. format_cell(footer, "footer-cell", has_outer_display and 5 or 3) | ||
else | else | ||
| Line 1,050: | Line 1,516: | ||
" edit]</span>)</sup>" | " edit]</span>)</sup>" | ||
return [=[{| class="floatright number-box" cellpadding="5" cellspacing="0" style="background: var(--wikt-palette-white, #ffffff); color: inherit; border: 1px #aaa solid; border-collapse: collapse; margin-top: .5em;" rules="all" | return [=[{| class="floatright number-box" cellpadding="5" cellspacing="0" style="background: var(--wikt-palette-white, #ffffff); color: inherit; border: 1px var(--border-color-base,#aaa) solid; border-collapse: collapse; margin-top: .5em;" rules="all" | ||
|+ ''']=] .. title .. edit_link .. "'''\n" .. | |+ ''']=] .. title .. edit_link .. "'''\n" .. | ||
upper_display .. '|- style="text-align: center;"\n' .. | upper_display .. '|- style="text-align: center;"\n' .. | ||
| Line 1,121: | Line 1,587: | ||
local optional2_term = args.opt2x; local optional2_alt = args.opt2xalt; local optional2_tr = args.opt2xtr | local optional2_term = args.opt2x; local optional2_alt = args.opt2xalt; local optional2_tr = args.opt2xtr | ||
local subpage = mw. | track(lang:getCode()) | ||
if sc then | |||
track("sc") | |||
end | |||
if headlink then | |||
track("headlink") | |||
end | |||
if wplink then | |||
track("wplink") | |||
end | |||
if alt then | |||
track("alt") | |||
end | |||
if cardinal_alt or ordinal_alt or adverbial_alt or multiplier_alt or distributive_alt or collective_alt or fractional_alt or optional1_alt or optional2_alt then | |||
track("xalt") | |||
end | |||
local subpage = mw.loadData("Module:headword/data").pagename | |||
local is_reconstructed = lang:hasType("reconstructed") or mw.title.getCurrentTitle().nsText == "Reconstruction" | local is_reconstructed = lang:hasType("reconstructed") or mw.title.getCurrentTitle().nsText == "Reconstruction" | ||
| Line 1,200: | Line 1,688: | ||
if wplink then | if wplink then | ||
footer = | footer = | ||
"[[w:" .. lang:getCode() .. ": | "[[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 | ||