Difference between revisions of "Module:ConvertNumeric"
Jump to navigation
Jump to search
blackwiki>Dcoetzee (Fix negative zero cases - weren't all handled correctly before (e.g. in adj case, "negative zeroth") and my new change regressed the cases that worked) |
blackwiki>Johnuniq (introduce function callable from another module with numerator and denominator for fractions (not implemented); fix bug "1050|sp=us" was "one thousand AND fifty"; clean "local" and whitespace) |
||
| Line 177: | Line 177: | ||
} | } | ||
| − | roman_numerals = { | + | local roman_numerals = { |
I = 1, | I = 1, | ||
V = 5, | V = 5, | ||
| Line 188: | Line 188: | ||
-- Converts a given valid roman numeral (and some invalid roman numerals) to a number. Returns -1, errorstring on error | -- Converts a given valid roman numeral (and some invalid roman numerals) to a number. Returns -1, errorstring on error | ||
| − | function roman_to_numeral(roman) | + | local function roman_to_numeral(roman) |
if type(roman) ~= "string" then return -1, "roman numeral not a string" end | if type(roman) ~= "string" then return -1, "roman numeral not a string" end | ||
local rev = roman:reverse() | local rev = roman:reverse() | ||
| Line 215: | Line 215: | ||
-- Converts a given integer between 0 and 100 to English text (e.g. 47 -> forty-seven) | -- Converts a given integer between 0 and 100 to English text (e.g. 47 -> forty-seven) | ||
| − | function numeral_to_english_less_100(num, ordinal, plural, zero) | + | local function numeral_to_english_less_100(num, ordinal, plural, zero) |
local terminal_ones, terminal_tens | local terminal_ones, terminal_tens | ||
if ordinal then | if ordinal then | ||
| Line 239: | Line 239: | ||
end | end | ||
| − | function standard_suffix(ordinal, plural) | + | local function standard_suffix(ordinal, plural) |
if ordinal then return 'th' end | if ordinal then return 'th' end | ||
if plural then return 's' end | if plural then return 's' end | ||
| Line 246: | Line 246: | ||
-- Converts a given integer (in string form) between 0 and 1000 to English text (e.g. 47 -> forty-seven) | -- Converts a given integer (in string form) between 0 and 1000 to English text (e.g. 47 -> forty-seven) | ||
| − | function numeral_to_english_less_1000(num, use_and, ordinal, plural, zero) | + | local function numeral_to_english_less_1000(num, use_and, ordinal, plural, zero) |
num = tonumber(num) | num = tonumber(num) | ||
if num < 100 then | if num < 100 then | ||
| Line 259: | Line 259: | ||
-- Converts a number expressed as a string in scientific notation to a string in standard decimal notation | -- 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. | -- e.g. 1.23E5 -> 123000, 1.23E-5 = .0000123. Conversion is exact, no rounding is performed. | ||
| − | function scientific_notation_to_decimal(num) | + | local function scientific_notation_to_decimal(num) |
local exponent, subs = num:gsub("^%-?%d*%.?%d*%-?[Ee]([+%-]?%d+)$", "%1") | 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 | if subs == 0 then return num end -- Input not in scientific notation, just return unmodified | ||
| Line 265: | Line 265: | ||
local negative = num:find("^%-") | local negative = num:find("^%-") | ||
| − | _, decimal_pos = num:find("%.") | + | local _, decimal_pos = num:find("%.") |
-- Mantissa will consist of all decimal digits with no decimal point | -- Mantissa will consist of all decimal digits with no decimal point | ||
local mantissa = num:gsub("^%-?(%d*)%.?(%d*)%-?[Ee][+%-]?%d+$", "%1%2") | local mantissa = num:gsub("^%-?(%d*)%.?(%d*)%-?[Ee][+%-]?%d+$", "%1%2") | ||
| Line 271: | Line 271: | ||
if not decimal_pos then decimal_pos = #mantissa + 1 end | if not decimal_pos then decimal_pos = #mantissa + 1 end | ||
local prev_len = #num | local prev_len = #num | ||
| − | + | ||
-- Remove leading zeros unless decimal point is in first position | -- Remove leading zeros unless decimal point is in first position | ||
while decimal_pos > 1 and mantissa:sub(1,1) == '0' do | while decimal_pos > 1 and mantissa:sub(1,1) == '0' do | ||
| Line 297: | Line 297: | ||
exponent = exponent + 1 | exponent = exponent + 1 | ||
end | end | ||
| − | + | ||
-- Insert decimal point in correct position and return | -- Insert decimal point in correct position and return | ||
return (negative and '-' or '') .. mantissa:sub(1, decimal_pos - 1) .. '.' .. mantissa:sub(decimal_pos) | return (negative and '-' or '') .. mantissa:sub(1, decimal_pos - 1) .. '.' .. mantissa:sub(decimal_pos) | ||
end | end | ||
| − | -- Rounds a number to the nearest integer | + | -- Rounds a number to the nearest integer (NOT USED) |
| − | function round_num(x) | + | local function round_num(x) |
if x%1 >= 0.5 then | if x%1 >= 0.5 then | ||
return math.ceil(x) | return math.ceil(x) | ||
| − | else | + | else |
return math.floor(x) | return math.floor(x) | ||
end | end | ||
| Line 315: | Line 315: | ||
-- Larger numbers will be rounded to a number with only one nonzero digit in front and all other digits zero. | -- 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. | -- Negative sign is preserved and does not count towards word limit. | ||
| − | function round_for_english(num, round) | + | local function round_for_english(num, round) |
-- If an integer with at most two digits, just return | -- If an integer with at most two digits, just return | ||
if num:find("^%-?%d?%d%.?$") then return num end | if num:find("^%-?%d?%d%.?$") then return num end | ||
| Line 362: | Line 362: | ||
-- Takes a decimal number and converts it to English text. | -- Takes a decimal number and converts it to English text. | ||
| − | -- num (string): the number to convert. Can be arbitrarily large decimal, such as "-123456789123456789.345", | + | -- 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 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". | -- 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 no whole number before 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') | -- 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 | -- use_and (boolean): whether to use the word 'and' between tens/ones place and higher places | ||
| Line 373: | Line 377: | ||
-- negative_word: Word to use for negative sign (typically 'negative' or 'minus') | -- negative_word: Word to use for negative sign (typically 'negative' or 'minus') | ||
-- round: nil or '': no rounding; 'on': round to nearest two-word number; 'up'/'down': round up/down to two-word number | -- round: nil or '': no rounding; 'on': round to nearest two-word number; 'up'/'down': round up/down to two-word number | ||
| − | function _numeral_to_english(num, capitalize, use_and, hyphenate, ordinal, plural, links, negative_word, round, zero) | + | local function _numeral_to_english(num, numerator, denominator, capitalize, use_and, hyphenate, ordinal, plural, links, negative_word, round, zero) |
num = scientific_notation_to_decimal(num) | num = scientific_notation_to_decimal(num) | ||
| Line 386: | Line 390: | ||
if num == '' and decimal_places then num = '0' end | if num == '' and decimal_places then num = '0' end | ||
if subs == 0 or num == '' then return 'Invalid decimal numeral' end | if subs == 0 or num == '' then return 'Invalid decimal numeral' end | ||
| − | + | ||
-- For each group of 3 digits except the last one, print with appropriate group name (e.g. million) | -- For each group of 3 digits except the last one, print with appropriate group name (e.g. million) | ||
local s = '' | local s = '' | ||
| Line 395: | Line 399: | ||
local group_digits = #num - group_num*3 | local group_digits = #num - group_num*3 | ||
s = s .. numeral_to_english_less_1000(num:sub(1, group_digits), false, false, false, zero) .. ' ' | s = s .. numeral_to_english_less_1000(num:sub(1, group_digits), false, false, false, zero) .. ' ' | ||
| − | if ((links == 'on' and group_num >= 3) or links:find(group)) and group_num <= 13 then | + | 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 .. ']]' | s = s .. '[[Orders_of_magnitude_(numbers)#10' .. group_num*3 .. '|' .. group .. ']]' | ||
else | else | ||
| Line 403: | Line 407: | ||
num = num:gsub("^0*", "") -- Trim leading zeros | num = num:gsub("^0*", "") -- Trim leading zeros | ||
end | end | ||
| − | + | ||
-- Handle final three digits of integer part | -- Handle final three digits of integer part | ||
| − | + | ||
if s ~= '' and num ~= '' then | if s ~= '' and num ~= '' then | ||
| − | if #num | + | if #num <= 2 and use_and then |
| + | s = s .. ' and ' | ||
| + | else | ||
s = s .. ' ' | s = s .. ' ' | ||
| − | |||
| − | |||
end | end | ||
end | end | ||
| Line 419: | Line 423: | ||
s = s .. standard_suffix(ordinal, plural) | s = s .. standard_suffix(ordinal, plural) | ||
end | end | ||
| − | + | ||
-- For decimal places (if any) output "point" followed by spelling out digit by digit | -- For decimal places (if any) output "point" followed by spelling out digit by digit | ||
if decimal_places then | if decimal_places then | ||
| Line 427: | Line 431: | ||
end | end | ||
end | end | ||
| − | + | ||
s = s:gsub("^%s*(.-)%s*$", "%1") -- Trim whitespace | s = s:gsub("^%s*(.-)%s*$", "%1") -- Trim whitespace | ||
if ordinal and plural then s = s .. 's' end -- s suffix works for all ordinals | if ordinal and plural then s = s .. 's' end -- s suffix works for all ordinals | ||
| Line 434: | Line 438: | ||
if hyphenate then s = s:gsub("%s", "-") end | if hyphenate then s = s:gsub("%s", "-") end | ||
if capitalize then s = s:gsub("^%l", string.upper) end | if capitalize then s = s:gsub("^%l", string.upper) end | ||
| − | + | ||
return s | return s | ||
end | end | ||
| − | local p = {} | + | local p = { spell_number = _numeral_to_english } -- can be called from another module |
function p.numeral_to_english(frame) | function p.numeral_to_english(frame) | ||
| Line 451: | Line 455: | ||
-- Convert all args passed through frame into normal arguments to helper function | -- Convert all args passed through frame into normal arguments to helper function | ||
| + | local numerator = frame.args['numerator'] | ||
| + | local denominator = frame.args['denominator'] | ||
local case = frame.args['case'] | local case = frame.args['case'] | ||
local sp = frame.args['sp'] | local sp = frame.args['sp'] | ||
| Line 458: | Line 464: | ||
local lk = frame.args['lk'] or '' | local lk = frame.args['lk'] or '' | ||
local negative = frame.args['negative'] or 'negative' | local negative = frame.args['negative'] or 'negative' | ||
| − | local round = frame.args['round'] or nil | + | local round = frame.args['round'] or nil |
local zero = frame.args['zero'] or nil | local zero = frame.args['zero'] or nil | ||
| − | return _numeral_to_english(num, case == 'U' or case == 'u', sp ~= 'us', adj == 'on', ord == 'on', pl == 'on', lk, negative, round, zero) | + | return _numeral_to_english(num, numerator, denominator, case == 'U' or case == 'u', sp ~= 'us', adj == 'on', ord == 'on', pl == 'on', lk, negative, round, zero) or '' |
end | end | ||
---- recursive function for p.decToHex | ---- recursive function for p.decToHex | ||
| − | function decToHexDigit(dec) | + | local function decToHexDigit(dec) |
local dig = {"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"} | 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 div = math.floor(dec/16) | ||
| Line 479: | Line 485: | ||
local minlength=args.minlength or pargs.minlength or 1 | local minlength=args.minlength or pargs.minlength or 1 | ||
minlength=tonumber(minlength) | minlength=tonumber(minlength) | ||
| − | prowl=mw.ustring.gmatch(text,"(.-)(%d+)") | + | local prowl=mw.ustring.gmatch(text,"(.-)(%d+)") |
local output="" | local output="" | ||
repeat | repeat | ||
Revision as of 22:34, 16 July 2013
| This module is rated as ready for general use. It has reached a mature form and is thought to be bug-free and ready for use wherever appropriate. It is ready to mention on help pages and other Wikipedia resources as an option for new users to learn. To reduce server load and bad output, it should be improved by sandbox testing rather than repeated trial-and-error editing. |
| This module is used in MediaWiki:Watchlist-messages. Changes to it can cause immediate changes to the Wikipedia user interface. To avoid large-scale disruption, any changes should first be tested in this module's /sandbox or /testcases subpage, or in your own user space. The tested changes can then be added in one single edit to this module. Please discuss any changes on the talk page before implementing them. |
| This Lua module is used on approximately 22,000 pages and changes may be widely noticed. Test changes in the module's /sandbox or /testcases subpages. Consider discussing changes on the talk page before implementing them.
Transclusion count updated automatically (see documentation). |
Usage
{{#invoke:ConvertNumeric|function_name}}
See also
- {{Spellnum per MOS}}
- {{Number to word}}
- Module:StripToNumbers - extract a number from a string (supports negatives and decimals) and return it, or optionally return a halved value
-- Module for converting between different representations of numbers. See talk page for user documentation.
-- For unit tests see: [[Module:ConvertNumeric/tests]]
local ones_position = {
[0] = 'zero',
[1] = 'one',
[2] = 'two',
[3] = 'three',
[4] = 'four',
[5] = 'five',
[6] = 'six',
[7] = 'seven',
[8] = 'eight',
[9] = 'nine',
[10] = 'ten',
[11] = 'eleven',
[12] = 'twelve',
[13] = 'thirteen',
[14] = 'fourteen',
[15] = 'fifteen',
[16] = 'sixteen',
[17] = 'seventeen',
[18] = 'eighteen',
[19] = 'nineteen'
}
local ones_position_ord = {
[0] = 'zeroth',
[1] = 'first',
[2] = 'second',
[3] = 'third',
[4] = 'fourth',
[5] = 'fifth',
[6] = 'sixth',
[7] = 'seventh',
[8] = 'eighth',
[9] = 'ninth',
[10] = 'tenth',
[11] = 'eleventh',
[12] = 'twelfth',
[13] = 'thirteenth',
[14] = 'fourteenth',
[15] = 'fifteenth',
[16] = 'sixteenth',
[17] = 'seventeenth',
[18] = 'eighteenth',
[19] = 'nineteenth'
}
local ones_position_plural = {
[0] = 'zeros',
[1] = 'ones',
[2] = 'twos',
[3] = 'threes',
[4] = 'fours',
[5] = 'fives',
[6] = 'sixes',
[7] = 'sevens',
[8] = 'eights',
[9] = 'nines',
[10] = 'tens',
[11] = 'elevens',
[12] = 'twelves',
[13] = 'thirteens',
[14] = 'fourteens',
[15] = 'fifteens',
[16] = 'sixteens',
[17] = 'seventeens',
[18] = 'eighteens',
[19] = 'nineteens'
}
local tens_position = {
[2] = 'twenty',
[3] = 'thirty',
[4] = 'forty',
[5] = 'fifty',
[6] = 'sixty',
[7] = 'seventy',
[8] = 'eighty',
[9] = 'ninety'
}
local tens_position_ord = {
[2] = 'twentieth',
[3] = 'thirtieth',
[4] = 'fortieth',
[5] = 'fiftieth',
[6] = 'sixtieth',
[7] = 'seventieth',
[8] = 'eightieth',
[9] = 'ninetieth'
}
local tens_position_plural = {
[2] = 'twenties',
[3] = 'thirties',
[4] = 'forties',
[5] = 'fifties',
[6] = 'sixties',
[7] = 'seventies',
[8] = 'eighties',
[9] = 'nineties'
}
local groups = {
[1] = 'thousand',
[2] = 'million',
[3] = 'billion',
[4] = 'trillion',
[5] = 'quadrillion',
[6] = 'quintillion',
[7] = 'sextillion',
[8] = 'septillion',
[9] = 'octillion',
[10] = 'nonillion',
[11] = 'decillion',
[12] = 'undecillion',
[13] = 'duodecillion',
[14] = 'tredecillion',
[15] = 'quattuordecillion',
[16] = 'quindecillion',
[17] = 'sexdecillion',
[18] = 'septendecillion',
[19] = 'octodecillion',
[20] = 'novemdecillion',
[21] = 'vigintillion',
[22] = 'unvigintillion',
[23] = 'duovigintillion',
[24] = 'tresvigintillion',
[25] = 'quattuorvigintillion',
[26] = 'quinquavigintillion',
[27] = 'sesvigintillion',
[28] = 'septemvigintillion',
[29] = 'octovigintillion',
[30] = 'novemvigintillion',
[31] = 'trigintillion',
[32] = 'untrigintillion',
[33] = 'duotrigintillion',
[34] = 'trestrigintillion',
[35] = 'quattuortrigintillion',
[36] = 'quinquatrigintillion',
[37] = 'sestrigintillion',
[38] = 'septentrigintillion',
[39] = 'octotrigintillion',
[40] = 'noventrigintillion',
[41] = 'quadragintillion',
[51] = 'quinquagintillion',
[61] = 'sexagintillion',
[71] = 'septuagintillion',
[81] = 'octogintillion',
[91] = 'nonagintillion',
[101] = 'centillion',
[102] = 'uncentillion',
[103] = 'duocentillion',
[104] = 'trescentillion',
[111] = 'decicentillion',
[112] = 'undecicentillion',
[121] = 'viginticentillion',
[122] = 'unviginticentillion',
[131] = 'trigintacentillion',
[141] = 'quadragintacentillion',
[151] = 'quinquagintacentillion',
[161] = 'sexagintacentillion',
[171] = 'septuagintacentillion',
[181] = 'octogintacentillion',
[191] = 'nonagintacentillion',
[201] = 'ducentillion',
[301] = 'trecentillion',
[401] = 'quadringentillion',
[501] = 'quingentillion',
[601] = 'sescentillion',
[701] = 'septingentillion',
[801] = 'octingentillion',
[901] = 'nongentillion',
[1001] = 'millinillion',
}
local roman_numerals = {
I = 1,
V = 5,
X = 10,
L = 50,
C = 100,
D = 500,
M = 1000
}
-- 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 'th' end
if plural then return 's' 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] .. ' hundred' .. standard_suffix(ordinal, plural)
else
return ones_position[math.floor(num/100)] .. ' hundred ' .. (use_and and 'and ' or '') .. numeral_to_english_less_100(num % 100, ordinal, plural, zero)
end
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
local prev_len = #num
-- 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
local small_int_len = #small_int
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
-- 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 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 no whole number before 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 "billion" and larger to Orders of magnitude article;
-- any other text: list of numbers to link (e.g. billion,quadrillion)
-- negative_word: Word to use for negative sign (typically 'negative' or 'minus')
-- round: nil or '': no rounding; 'on': round to nearest two-word number; 'up'/'down': round up/down to two-word number
local function _numeral_to_english(num, numerator, denominator, capitalize, use_and, hyphenate, ordinal, plural, links, negative_word, round, zero)
num = scientific_notation_to_decimal(num)
if round and round ~= '' and round ~= 'on' and round ~= 'up' and round ~= 'down' then return 'Invalid rounding mode' end
if round and round ~= '' then num = round_for_english(num, round) end
-- Separate into negative sign, num (digits before decimal), decimal_places (digits after decimal)
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 return 'Invalid decimal numeral' end
-- For each group of 3 digits except the last one, print with appropriate group name (e.g. million)
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 million" 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 .. ' point'
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", "zero")
if hyphenate then s = s:gsub("%s", "-") end
if capitalize then s = s:gsub("^%l", string.upper) end
return s
end
local p = { spell_number = _numeral_to_english } -- can be called from another module
function p.numeral_to_english(frame)
local num = frame.args[1]
num = num:gsub("^%s*(.-)%s*$", "%1") -- Trim whitespace
num = num:gsub(",", "") -- Remove commas
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
-- Convert all args passed through frame into normal arguments to helper function
local numerator = frame.args['numerator']
local denominator = frame.args['denominator']
local case = frame.args['case']
local sp = frame.args['sp']
local adj = frame.args['adj']
local ord = frame.args['ord']
local pl = frame.args['pl']
local lk = frame.args['lk'] or ''
local negative = frame.args['negative'] or 'negative'
local round = frame.args['round'] or nil
local zero = frame.args['zero'] or nil
return _numeral_to_english(num, numerator, denominator, case == 'U' or case == 'u', sp ~= 'us', adj == 'on', ord == 'on', pl == 'on', lk, negative, round, zero) 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