Lompat ke isi

Modul:ConvertNumeric/sandbox

Dari Wikipedia bahasa Indonesia, ensiklopedia bebas

-- Module for converting between different representations of numbers. See talk page for user documentation.
-- For unit tests see: [[Module:ConvertNumeric/testcases]]
-- When editing, preview with: [[Module_talk:ConvertNumeric/testcases]]
-- First, edit [[Module:ConvertNumeric/sandbox]], then preview with [[Module_talk:ConvertNumeric/sandbox/testcases]]

local ones_position = {
  [0] = 'nol',
  [1] = 'satu',
  [2] = 'dua',
  [3] = 'tiga',
  [4] = 'empat',
  [5] = 'lima',
  [6] = 'enam',
  [7] = 'tujuh',
  [8] = 'delapan',
  [9] = 'sembilan',
  [10] = 'sepuluh',
  [11] = 'sebelas',
  [12] = 'dua belas',
  [13] = 'tiga belas',
  [14] = 'empat belas',
  [15] = 'lima belas',
  [16] = 'enam belas',
  [17] = 'tujuh belas',
  [18] = 'delapan belas',
  [19] = 'sembilan belas'
}

local ones_position_ord = {
  [0] = 'kenol',
  [1] = 'pertama',
  [2] = 'kedua',
  [3] = 'ketiga',
  [4] = 'keempat',
  [5] = 'kelima',
  [6] = 'keenam',
  [7] = 'ketujuh',
  [8] = 'kedelapan',
  [9] = 'kesembilan',
  [10] = 'kesepuluh',
  [11] = 'kesebelas',
  [12] = 'kedua belas',
  [13] = 'ketiga belas',
  [14] = 'keempat belas',
  [15] = 'kelima belas',
  [16] = 'keenam belas',
  [17] = 'ketujuh belas',
  [18] = 'kedelapan belas',
  [19] = 'kesembilan belas'
}

local ones_position_plural = {
  [0] = 'nolan',
  [1] = 'satuan',
  [2] = 'duaan',
  [3] = 'tigaan',
  [4] = 'empatan',
  [5] = 'limaan',
  [6] = 'enaman',
  [7] = 'tujuhan',
  [8] = 'delapanan',
  [9] = 'sembilanan',
  [10] = 'sepuluhan',
  [11] = 'sebelasan',
  [12] = 'dua belasan',
  [13] = 'tiga belasan',
  [14] = 'empat belasan',
  [15] = 'lima belasan',
  [16] = 'enam belasan',
  [17] = 'tujuh belasan',
  [18] = 'delapan belasan',
  [19] = 'sembilan belasan'
}

local tens_position = {
  [2] = 'dua puluh',
  [3] = 'tiga puluh',
  [4] = 'empat puluh',
  [5] = 'lima puluh',
  [6] = 'enam puluh',
  [7] = 'tujuh puluh',
  [8] = 'delapan puluh',
  [9] = 'sembilan puluh'
}

local tens_position_ord = {
  [2] = 'kedua puluh',
  [3] = 'ketiga puluh',
  [4] = 'keempat puluh',
  [5] = 'kelima puluh',
  [6] = 'keenam puluh',
  [7] = 'ketujuh puluh',
  [8] = 'kedelapan puluh',
  [9] = 'kesembilan puluh'
}

local tens_position_plural = {
  [2] = 'dua puluhan',
  [3] = 'tiga puluhan',
  [4] = 'empat puluhan',
  [5] = 'lima puluhan',
  [6] = 'enam puluhan',
  [7] = 'tujuh puluhan',
  [8] = 'delapan puluhan',
  [9] = 'sembilan puluhan'
}

local groups = {
  [1] = 'ribu',
  [2] = 'juta',
  [3] = 'miliar',
  [4] = 'triliun',
  [5] = 'kuadriliun',
  [6] = 'kuintiliun',
  [7] = 'sekstiliun',
  [8] = 'septiliun',
  [9] = 'oktiliun',
  [10] = 'noniliun',
  [11] = 'desiliun',
  [12] = 'undesiliun',
  [13] = 'duodesiliun',
  [14] = 'tredesiliun',
  [15] = 'kuatuordesiliun',
  [16] = 'kuindesiliun',
  [17] = 'seksdesiliun',
  [18] = 'septendesiliun',
  [19] = 'oktodesiliun',
  [20] = 'novemdesiliun',
  [21] = 'vigintiliun',
  [22] = 'unvigintiliun',
  [23] = 'duovigintiliun',
  [24] = 'tresvigintiliun',
  [25] = 'kuatuorvigintiliun',
  [26] = 'kuinquavigintiliun',
  [27] = 'sesvigintiliun',
  [28] = 'septemvigintiliun',
  [29] = 'oktovigintiliun',
  [30] = 'novemvigintiliun',
  [31] = 'trigintiliun',
  [32] = 'untrigintiliun',
  [33] = 'duotrigintiliun',
  [34] = 'trestrigintiliun',
  [35] = 'kuatuortrigintiliun',
  [36] = 'kuinquatrigintiliun',
  [37] = 'sestrigintiliun',
  [38] = 'septentrigintiliun',
  [39] = 'oktotrigintiliun',
  [40] = 'noventrigintiliun',
  [41] = 'kuadragintiliun',
  [51] = 'kuinquagintiliun',
  [61] = 'seksagintiliun',
  [71] = 'septuagintiliun',
  [81] = 'oktogintiliun',
  [91] = 'nonagintiliun',
  [101] = 'sentiliun',
  [102] = 'unsentiliun',
  [103] = 'duosentiliun',
  [104] = 'tresentiliun',
  [111] = 'desisentiliun',
  [112] = 'undesisentiliun',
  [121] = 'vigintisentiliun',
  [122] = 'unvigintisentiliun',
  [131] = 'trigintasentiliun',
  [141] = 'kuadragintasentiliun',
  [151] = 'kuinquagintasentiliun',
  [161] = 'seksagintasentiliun',
  [171] = 'septuagintasentiliun',
  [181] = 'oktogintasentiliun',
  [191] = 'nonagintasentiliun',
  [201] = 'dusentiliun',
  [301] = 'tresentiliun',
  [401] = 'kuadringentiliun',
  [501] = 'kuingentiliun',
  [601] = 'sesentiliun',
  [701] = 'septingentiliun',
  [801] = 'oktingentiliun',
  [901] = 'nongentiliun',
  [1001] = 'miliniliun',
}

local roman_numerals = {
  I = 1,
  V = 5,
  X = 10,
  L = 50,
  C = 100,
  D = 500,
  M = 1000
}

local eng_lt20 = {
  ['zeroth']           =  0,
  ['first']            =  1,
  ['second']           =  2,
  ['third']            =  3,
  ['fourth']           =  4,
  ['fifth']            =  5,
  ['sixth']            =  6,
  ['seventh']          =  7,
  ['eighth']           =  8,
  ['ninth']            =  9,
  ['tenth']            = 10,
  ['eleventh']         = 11,
  ['twelfth']          = 12,
  ['thirteenth']       = 13,
  ['fourteenth']       = 14,
  ['fifteenth']        = 15,
  ['sixteenth']        = 16,
  ['seventeenth']      = 17,
  ['eighteenth']       = 18,
  ['nineteenth']       = 19,
  ['kenol']            =  0,
  ['pertama']          =  1,
  ['kedua']            =  2,
  ['ketiga']           =  3,
  ['keempat']          =  4,
  ['kelima']           =  5,
  ['keenam']           =  6,
  ['ketujuh']          =  7,
  ['kedelapan']        =  8,
  ['kesembilan']       =  9,
  ['kesepuluh']        = 10,
  ['kesebelas']        = 11,
  ['kedua belas']      = 12,
  ['ketiga belas']     = 13,
  ['keempat belas']    = 14,
  ['kelima belas']     = 15,
  ['keenam belas']     = 16,
  ['ketujuh belas']    = 17,
  ['kedelapan belas']  = 18,
  ['kesembilan belas'] = 19,
}
local eng_tens_end  = {
  ['twentieth']  = 20,
  ['thirtieth']  = 30,
  ['fortieth']   = 40,
  ['fiftieth']   = 50,
  ['sixtieth']   = 60,
  ['seventieth'] = 70,
  ['eightieth']  = 80,
  ['ninetieth']  = 90,
  ['kedua puluh']  = 20,
  ['ketiga puluh']  = 30,
  ['keempat puluh']   = 40,
  ['kelima puluh']   = 50,
  ['keenam puluh']   = 60,
  ['ketujuh puluh'] = 70,
  ['kedelapan puluh']  = 80,
  ['kesembilan puluh']  = 90,
}
local eng_tens_cont = {
  ['twenty']  = 20,
  ['thirty']  = 30,
  ['forty']   = 40,
  ['fifty']   = 50,
  ['sixty']   = 60,
  ['seventy'] = 70,
  ['eighty']  = 80,
  ['ninety']  = 90,
  ['dua puluh']  = 20,
  ['tiga puluh']  = 30,
  ['empat puluh']   = 40,
  ['lima puluh']   = 50,
  ['enam puluh']   = 60,
  ['tujuh puluh'] = 70,
  ['delapan puluh']  = 80,
  ['sembilan puluh']  = 90,
}

-- Converts a given valid roman numeral (and some invalid roman numerals) to a number. Returns -1, errorstring on error
local function roman_to_numeral(roman)
  if type(roman) ~= "string" then return -1, "roman numeral not a string" end
  local rev = roman:reverse()
  local raising = true
  local last = 0
  local result = 0
  for i = 1, #rev do
    local c = rev:sub(i, i)
    local next = roman_numerals[c]
    if next == nil then return -1, "roman numeral contains illegal character " .. c end
    if next > last then
      result = result + next
      raising = true
    elseif next < last then
      result = result - next
      raising = false
    elseif raising then
      result = result + next
    else
      result = result - next
    end
    last = next
  end
  return result
end

-- Converts a given integer between 0 and 100 to English text (e.g. 47 -> forty-seven)
local function numeral_to_english_less_100(num, ordinal, plural, zero)
  local terminal_ones, terminal_tens
  if ordinal then
    terminal_ones = ones_position_ord
    terminal_tens = tens_position_ord
  elseif plural then
    terminal_ones = ones_position_plural
    terminal_tens = tens_position_plural
  else
    terminal_ones = ones_position
    terminal_tens = tens_position
  end

  if num == 0 and zero ~= nil then
    return zero
  elseif num < 20 then
    return terminal_ones[num]
  elseif num % 10 == 0 then
    return terminal_tens[num / 10]
  else
    return tens_position[math.floor(num / 10)] .. ' ' .. terminal_ones[num % 10]
  end
end

local function standard_suffix(ordinal, plural)
  if ordinal then return '' end
  if plural then return '' end
  return ''
end

-- Converts a given integer (in string form) between 0 and 1000 to English text (e.g. 47 -> forty-seven)
local function numeral_to_english_less_1000(num, use_and, ordinal, plural, zero)
  num = tonumber(num)
  if num < 100 then
    return numeral_to_english_less_100(num, ordinal, plural, zero)
  elseif num % 100 == 0 then
    return ones_position[num/100] .. ' ratus' .. standard_suffix(ordinal, plural)
  else
    return ones_position[math.floor(num/100)] .. ' ratus ' .. (use_and and '' or '') .. numeral_to_english_less_100(num % 100, ordinal, plural, zero)
  end
end

-- Converts an English-text ordinal between 'zeroth' and 'ninety-ninth' to a number [0–99], else -1
local function english_to_ordinal(english)
  local eng = string.lower(english or '')
  
  local eng_lt20 = eng_lt20
  local eng_tens_end = eng_tens_end
  local eng_tens_cont = eng_tens_cont
  
  if eng_lt20[eng] then
    return eng_lt20[eng] --e.g. first -> 1
  elseif eng_tens_end[eng] then
    return eng_tens_end[eng] --e.g. ninetieth -> 90
  else
    local tens, ones = string.match(eng, '^([a-z]+)%-([a-z]+)$')
    if tens and ones then
      local tens_cont = eng_tens_cont[tens]
      local ones_end  = eng_lt20[ones]
      if tens_cont and ones_end then
        return tens_cont + ones_end --e.g. ninety-ninth -> 99
      end
    end
  end
  return -1 --failed
end

-- Converts a number expressed as a string in scientific notation to a string in standard decimal notation
-- e.g. 1.23E5 -> 123000, 1.23E-5 = .0000123. Conversion is exact, no rounding is performed.
local function scientific_notation_to_decimal(num)
  local exponent, subs = num:gsub("^%-?%d*%.?%d*%-?[Ee]([+%-]?%d+)$", "%1")
  if subs == 0 then return num end  -- Input not in scientific notation, just return unmodified
  exponent = tonumber(exponent)

  local negative = num:find("^%-")
  local _, decimal_pos = num:find("%.")
  -- Mantissa will consist of all decimal digits with no decimal point
  local mantissa = num:gsub("^%-?(%d*)%.?(%d*)%-?[Ee][+%-]?%d+$", "%1%2")
  if negative and decimal_pos then decimal_pos = decimal_pos - 1 end
  if not decimal_pos then decimal_pos = #mantissa + 1 end

  -- Remove leading zeros unless decimal point is in first position
  while decimal_pos > 1 and mantissa:sub(1,1) == '0' do
    mantissa = mantissa:sub(2)
    decimal_pos = decimal_pos - 1
  end
  -- Shift decimal point right for exponent > 0
  while exponent > 0 do
    decimal_pos = decimal_pos + 1
    exponent = exponent - 1
    if decimal_pos > #mantissa + 1 then mantissa = mantissa .. '0' end
    -- Remove leading zeros unless decimal point is in first position
    while decimal_pos > 1 and mantissa:sub(1,1) == '0' do
      mantissa = mantissa:sub(2)
      decimal_pos = decimal_pos - 1
    end
  end
  -- Shift decimal point left for exponent < 0
  while exponent < 0 do
    if decimal_pos == 1 then
      mantissa = '0' .. mantissa
    else
      decimal_pos = decimal_pos - 1
    end
    exponent = exponent + 1
  end

  -- Insert decimal point in correct position and return
  return (negative and '-' or '') .. mantissa:sub(1, decimal_pos - 1) .. '.' .. mantissa:sub(decimal_pos)
end

-- Rounds a number to the nearest integer (NOT USED)
local function round_num(x)
  if x%1 >= 0.5 then
    return math.ceil(x)
  else
    return math.floor(x)
  end
end

-- Rounds a number to the nearest two-word number (round = up, down, or "on" for round to nearest)
-- Numbers with two digits before the decimal will be rounded to an integer as specified by round.
-- Larger numbers will be rounded to a number with only one nonzero digit in front and all other digits zero.
-- Negative sign is preserved and does not count towards word limit.
local function round_for_english(num, round)
  -- If an integer with at most two digits, just return
  if num:find("^%-?%d?%d%.?$") then return num end

  local negative = num:find("^%-")
  if negative then
    -- We're rounding magnitude so flip it
    if round == 'up' then round = 'down' elseif round == 'down' then round = 'up' end
  end

  -- If at most two digits before decimal, round to integer and return
  local _, _, small_int, trailing_digits, round_digit = num:find("^%-?(%d?%d?)%.((%d)%d*)$")
  if small_int then
    if small_int == '' then small_int = '0' end
    if (round == 'up' and trailing_digits:find('[1-9]')) or (round == 'on' and tonumber(round_digit) >= 5) then
      small_int = tostring(tonumber(small_int) + 1)
    end
    return (negative and '-' or '') .. small_int
  end

  -- When rounding up, any number with > 1 nonzero digit will round up (e.g. 1000000.001 rounds up to 2000000)
  local nonzero_digits = 0
  for digit in num:gfind("[1-9]") do
    nonzero_digits = nonzero_digits + 1
  end

  num = num:gsub("%.%d*$", "") -- Remove decimal part
  -- Second digit used to determine which way to round lead digit
  local _, _, lead_digit, round_digit, round_digit_2, rest = num:find("^%-?(%d)(%d)(%d)(%d*)$")
  if tonumber(lead_digit .. round_digit) < 20 and (1 + #rest) % 3 == 0 then
    -- In English numbers < 20 are one word so put 2 digits in lead and round based on 3rd
    lead_digit = lead_digit .. round_digit
    round_digit = round_digit_2
  else
    rest = round_digit_2 .. rest
  end

  if (round == 'up' and nonzero_digits > 1) or (round == 'on' and tonumber(round_digit) >= 5) then
    lead_digit = tostring(tonumber(lead_digit) + 1)
  end
  -- All digits but lead digit will turn to zero
  rest = rest:gsub("%d", "0")
  return (negative and '-' or '') .. lead_digit .. '0' .. rest
end

local denominators = {
  [2] = { 'setengah' },
  [3] = { 'per tiga' },
  [4] = { 'per empat' },
  [5] = { 'per lima' },
  [6] = { 'per enam' },
  [8] = { 'per delapan' },
  [9] = { 'per sembilan' },
  [10] = { 'per sepuluh' },
  [16] = { 'per enam belas' },
}

-- Return status, fraction where:
-- status is a string:
--     "finished" if there is a fraction with no whole number;
--     "ok" if fraction is empty or valid;
--     "unsupported" if bad fraction;
-- fraction is a string giving (numerator / denominator) as English text, or is "".
-- Only unsigned fractions with a very limited range of values are supported,
-- except that if whole is empty, the numerator can use "-" to indicate negative.
-- whole (string or nil): nil or "" if no number before the fraction
-- numerator (string or nil): numerator, if any (default = 1 if a denominator is given)
-- denominator (string or nil): denominator, if any
-- sp_us (boolean): true if sp=us
-- negative_word (string): word to use for negative sign, if whole is empty
-- use_one (boolean): false: 2+1/2 → "two and a half"; true: "two and one-half"
local function fraction_to_english(whole, numerator, denominator, sp_us, negative_word, use_one)
  if numerator or denominator then
    local finished = (whole == nil or whole == '')
    local sign = ''
    if numerator then
      if finished and numerator:sub(1, 1) == '-' then
        numerator = numerator:sub(2)
        sign = negative_word .. ' '
      end
    else
      numerator = '1'
    end
    if not numerator:match('^%d+$') or not denominator or not denominator:match('^%d+$') then
      return 'unsupported', ''
    end
    numerator = tonumber(numerator)
    denominator = tonumber(denominator)
    local dendata = denominators[denominator]
    if not (dendata and 1 <= numerator and numerator <= 99) then
      return 'unsupported', ''
    end
    local numstr, denstr
    local sep = ' '
    if numerator == 1 then
      denstr = sp_us and dendata.us or dendata[1]
      if finished or use_one then
        numstr = 'satu'
      elseif denstr:match('^[aeiou]') then
        numstr = 'an'
        sep = ' '
      else
        numstr = 'a'
        sep = ' '
      end
    else
      numstr = numeral_to_english_less_100(numerator)
      denstr = dendata.plural
      if not denstr then
        denstr = (sp_us and dendata.us or dendata[1]) .. 's'
      end
    end
    if finished then
      return 'finished', sign .. numstr .. sep .. denstr
    end
    return 'ok', ' and ' .. numstr .. sep .. denstr
  end
  return 'ok', ''
end

-- Takes a decimal number and converts it to English text.
-- Return nil if a fraction cannot be converted (only some numbers are supported for fractions).
-- num (string or nil): the number to convert.
--      Can be an arbitrarily large decimal, such as "-123456789123456789.345", and
--      can use scientific notation (e.g. "1.23E5").
--      May fail for very large numbers not listed in "groups" such as "1E4000".
--      num is nil if there is no whole number before a fraction.
-- numerator (string or nil): numerator of fraction (nil if no fraction)
-- denominator (string or nil): denominator of fraction (nil if no fraction)
-- capitalize (boolean): whether to capitalize the result (e.g. 'One' instead of 'one')
-- use_and (boolean): whether to use the word 'and' between tens/ones place and higher places
-- hyphenate (boolean): whether to hyphenate all words in the result, useful for use as an adjective
-- ordinal (boolean): whether to produce an ordinal (e.g. 'first' instead of 'one')
-- plural (boolean): whether to pluralize the resulting number
-- links: nil: do not add any links; 'on': link "biliun" and larger to Orders of magnitude article;
--        any other text: list of numbers to link (e.g. "biliun,quadriliun")
-- negative_word: word to use for negative sign (typically 'negative' or 'minus'; nil to use default)
-- round: nil or '': no rounding; 'on': round to nearest two-word number; 'up'/'down': round up/down to two-word number
-- zero: word to use for value '0' (nil to use default)
-- use_one (boolean): false: 2+1/2 → "two and a half"; true: "two and one-half"
local function _numeral_to_english(num, numerator, denominator, capitalize, use_and, hyphenate, ordinal, plural, links, negative_word, round, zero, use_one)
  if not negative_word then
    if use_and then
      -- TODO Should 'minus' be used when do not have sp=us?
      --      If so, need to update testcases, and need to fix "minus zero".
      -- negative_word = 'minus'
      negative_word = 'negative'
    else
      negative_word = 'negative'
    end
  end
  local status, fraction_text = fraction_to_english(num, numerator, denominator, not use_and, negative_word, use_one)
  if status == 'unsupported' then
    return nil
  end
  if status == 'finished' then
    -- Input is a fraction with no whole number.
    -- Hack to avoid executing stuff that depends on num being a number.
    local s = fraction_text
    if hyphenate then s = s:gsub("%s", "-") end
    if capitalize then s = s:gsub("^%l", string.upper) end
    return s
  end
  num = scientific_notation_to_decimal(num)
  if round and round ~= '' then
    if round ~= 'on' and round ~= 'up' and round ~= 'down' then
      error("Invalid rounding mode")
    end
    num = round_for_english(num, round)
  end

  -- Separate into negative sign, num (digits before decimal), decimal_places (digits after decimal)
  local MINUS = '−'  -- Unicode U+2212 MINUS SIGN (may be in values from [[Module:Convert]])
  if num:sub(1, #MINUS) == MINUS then
    num = '-' .. num:sub(#MINUS + 1)  -- replace MINUS with '-'
  elseif num:sub(1, 1) == '+' then
    num = num:sub(2)  -- ignore any '+'
  end
  local negative = num:find("^%-")
  local decimal_places, subs = num:gsub("^%-?%d*%.(%d+)$", "%1")
  if subs == 0 then decimal_places = nil end
  num, subs = num:gsub("^%-?(%d*)%.?%d*$", "%1")
  if num == '' and decimal_places then num = '0' end
  if subs == 0 or num == '' then error("Invalid decimal numeral") end

  -- For each group of 3 digits except the last one, print with appropriate group name (e.g. miliun)
  local s = ''
  while #num > 3 do
    if s ~= '' then s = s .. ' ' end
    local group_num = math.floor((#num - 1) / 3)
    local group = groups[group_num]
    local group_digits = #num - group_num*3
    s = s .. numeral_to_english_less_1000(num:sub(1, group_digits), false, false, false, zero) .. ' '
    if links and (((links == 'on' and group_num >= 3) or links:find(group)) and group_num <= 13) then
      s = s .. '[[Orders_of_magnitude_(numbers)#10' .. group_num*3 .. '|' .. group .. ']]'
    else
      s = s .. group
    end
    num = num:sub(1 + group_digits)
    num = num:gsub("^0*", "")  -- Trim leading zeros
  end

  -- Handle final three digits of integer part
  if s ~= '' and num ~= '' then
    if #num <= 2 and use_and then
      s = s .. ' and '
    else
      s = s .. ' '
    end
  end
  if s == '' or num ~= '' then
    s = s .. numeral_to_english_less_1000(num, use_and, ordinal, plural, zero)
  elseif ordinal or plural then
    -- Round numbers like "one miliun" take standard suffixes for ordinal/plural
    s = s .. standard_suffix(ordinal, plural)
  end

  -- For decimal places (if any) output "point" followed by spelling out digit by digit
  if decimal_places then
    s = s .. ' koma'
    for i = 1, #decimal_places do
      s = s .. ' ' .. ones_position[tonumber(decimal_places:sub(i,i))]
    end
  end

  s = s:gsub("^%s*(.-)%s*$", "%1")   -- Trim whitespace
  if ordinal and plural then s = s .. 's' end  -- s suffix works for all ordinals
  if negative and s ~= zero then s = negative_word .. ' ' .. s end
  s = s:gsub("negative zero", "nol")
  s = s .. fraction_text
  if hyphenate then s = s:gsub("%s", " ") end
  if capitalize then s = s:gsub("^%l", string.upper) end
  return s
end

local p = {  -- Functions that can be called from another module
  roman_to_numeral = roman_to_numeral,
  spell_number = _numeral_to_english,
  english_to_ordinal = english_to_ordinal,
}

function p._roman_to_numeral(frame) -- Callable via {{#invoke:ConvertNumeric|_roman_to_numeral|VI}}
  return roman_to_numeral(frame.args[1])
end

function p._english_to_ordinal(frame) -- callable via {{#invoke:ConvertNumeric|_english_to_ordinal|First}}
  return english_to_ordinal(frame.args[1])
end

function p.numeral_to_english(frame)
  local args = frame.args
  local num = args[1]
  num = num:gsub("^%s*(.-)%s*$", "%1")   -- Trim whitespace
  num = num:gsub(",", "")   -- Remove commas
  num = num:gsub("^<span[^<>]*></span>", "") -- Generated by Template:age
  if num ~= '' then  -- a fraction may have an empty whole number
    if not num:find("^%-?%d*%.?%d*%-?[Ee]?[+%-]?%d*$") then
      -- Input not in a valid format, try to pass it through #expr to see
      -- if that produces a number (e.g. "3 + 5" will become "8").
      num = frame:preprocess('{{#expr: ' .. num .. '}}')
    end
  end

  -- Pass args from frame to helper function
  return _numeral_to_english(
    num,
    args['numerator'],
    args['denominator'],
    args['case'] == 'U' or args['case'] == 'u',
    args['sp'] ~= 'us',
    args['adj'] == 'on',
    args['ord'] == 'on',
    args['pl'] == 'on',
    args['lk'],
    args['negative'],
    args['round'],
    args['zero'],
    args['one'] == 'one'  -- experiment: using '|one=one' makes fraction 2+1/2 give "two and one-half" instead of "two and a half"
  ) or ''
end

---- recursive function for p.decToHex
local function decToHexDigit(dec)
  local dig = {"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"}
  local div = math.floor(dec/16)
  local mod = dec-(16*div)
  if div >= 1 then return decToHexDigit(div)..dig[mod+1] else return dig[mod+1] end
end -- I think this is supposed to be done with a tail call but first I want something that works at all
---- finds all the decimal numbers in the input text and hexes each of them
function p.decToHex(frame)
  local args=frame.args
  local parent=frame.getParent(frame)
  local pargs={}
  if parent then pargs=parent.args end
  local text=args[1] or pargs[1] or ""
  local minlength=args.minlength or pargs.minlength or 1
  minlength=tonumber(minlength)
  local prowl=mw.ustring.gmatch(text,"(.-)(%d+)")
  local output=""
  repeat
    local chaff,dec=prowl()
    if not(dec) then break end
    local hex=decToHexDigit(dec)
    while (mw.ustring.len(hex)<minlength) do hex="0"..hex end
    output=output..chaff..hex
  until false
  local chaff=mw.ustring.match(text,"(%D+)$") or ""
  return output..chaff
end

return p