มอดูล:ConvertNumeric

-- 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 = '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
	}
	
	local engord_tens_end  = {
		'twentieth'	= 20,
		'thirtieth'	= 30,
		'fortieth'	= 40,
		'fiftieth'	= 50,
		'sixtieth'	= 60,
		'seventieth'	= 70,
		'eightieth'	= 80,
		'ninetieth'	= 90,
	}
	
	local eng_tens_cont = {
		'twenty'  = 20,
		'thirty'  = 30,
		'forty'   = 40,
		'fifty'   = 50,
		'sixty'   = 60,
		'seventy' = 70,
		'eighty'  = 80,
		'ninety'  = 90,
	}
	
	local denominators = {
		2 = { 'half', plural = 'halves' },
		3 = { 'third' },
		4 = { 'quarter', us = 'fourth' },
		5 = { 'fifth' },
		6 = { 'sixth' },
		8 = { 'eighth' },
		9 = { 'ninth' },
		10 = { 'tenth' },
		16 = { 'sixteenth' },
	}
	
	local thai_ones_position = {
		0 = "ศูนย์",
		1 = "หนึ่ง",
		2 = "สอง",
		3 = "สาม",
		4 = "สี่",
		5 = "ห้า",
		6 = "หก",
		7 = "เจ็ด",
		8 = "แปด",
		9 = "เก้า"
	}
	
	local data = {
		'normal' = {
			0 = ,
			1 = 'หนึ่ง',
			2 = 'สอง',
			3 = 'สาม',
			4 = 'สี่',
			5 = 'ห้า',
			6 = 'หก',
			7 = 'เจ็ด',
			8 = 'แปด',
			9 = 'เก้า',
		},
		0 = {
			0 = ,
			1 = 'เอ็ด',
			2 = 'สอง',
			3 = 'สาม',
			4 = 'สี่',
			5 = 'ห้า',
			6 = 'หก',
			7 = 'เจ็ด',
			8 = 'แปด',
			9 = 'เก้า',
			'value' = ,
			'mode' = 1,
		},
		1 = {
			0 = ,
			1 = ' ',
			2 = 'ยี่',
			3 = 'สาม',
			4 = 'สี่',
			5 = 'ห้า',
			6 = 'หก',
			7 = 'เจ็ด',
			8 = 'แปด',
			9 = 'เก้า',
			'value' = 'สิบ',
			'mode' = 1,
		},
		2 = {
			'value' = 'ร้อย',
			'mode' = 0,
		},
		3 = {
			'value' = 'พัน',
			'mode' = 0,
		},
		4 = {
			'value' = 'หมื่น',
			'mode' = 0,
		},
		5 = {
			'value' = 'แสน',
			'mode' = 0,
		},
		6 = {
			0 = ,
			1 = 'เอ็ด',
			2 = 'สอง',
			3 = 'สาม',
			4 = 'สี่',
			5 = 'ห้า',
			6 = 'หก',
			7 = 'เจ็ด',
			8 = 'แปด',
			9 = 'เก้า',
			'value' = 'ล้าน',
			'mode' = 1,
		},
	}
	
	-- 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_numeralsc
			if next == nil then return -1, "roman numeral contains illegal character " .. c end
			if next > last then
				result = result + next
				raising = true
			elseif next  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  forty-seven)
	local function numeral_to_english_less_1000(num, use_and, ordinal, plural, zero)
		num = tonumber(num)
		if num math.huge"
		end
		if num == '0' then
			return 'ศูนย์'
		end
		local level = 0
		result = {}
		for i = #num, 1, -1 do
			table.insert(result, datalevel'value')
			if datalevel'mode' == 0 then
				table.insert(result, data'normal'tonumber(num:sub(i, i)))
			else
				table.insert(result, dataleveltonumber(num:sub(i, i)))
			end
			level = level + 1
			if level > 6 then
				level = 1
			end
		end
		for i = 1, #result do
			if resulti == 'เอ็ด' and resulti + 2 == nil then
				resulti = 'หนึ่ง'
			end
			if i % 2 == 0 and resulti ==  and (i > 2 and (i - 2) % 12 ~= 0) then
				resulti - 1 = 
			end
			if resulti == ' ' then
				resulti = 
			end
		end
		newresult = {}
		for i = #result, 1, -1 do
			table.insert(newresult, resulti)
		end
		return table.concat(newresult)
	end
	
		-- Converts an ordinal in English text from 'zeroth' to 'ninety-ninth' inclusive to a number 0–99, else -1.
		local function english_to_ordinal(english)
		local eng = string.lower(english or )
		local engord_lt20 = {} -- ones_position_ord{} keys & values swapped
		for k, v in pairs( ones_position_ord ) do
			engord_lt20v = k
		end
		if engord_lt20eng then
			return engord_lt20eng -- e.g. first -> 1
		elseif engord_tens_endeng then
			return engord_tens_endeng -- e.g. ninetieth -> 90
		else
			local tens, ones = string.match(eng, '^(a-z+)%s%-+(a-z+)$')
			if tens and ones then
				local tens_cont = eng_tens_conttens
				local ones_end  = engord_lt20ones
				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 in English text from 'zero' to 'ninety-nine' inclusive to a number 0–99, else -1.
	local function english_to_numeral(english)
		local eng = string.lower(english or )
		local eng_lt20 = { 'single' = 1 } -- ones_position{} keys & values swapped
		for k, v in pairs( ones_position ) do
			eng_lt20v = k
		end
		if eng_lt20eng then
			return eng_lt20eng -- e.g. one -> 1
		elseif eng_tens_conteng then
			return eng_tens_conteng -- e.g. ninety -> 90
		else
			local tens, ones = string.match(eng, '^(a-z+)%s%-+(a-z+)$')
			if tens and ones then
				local tens_cont = eng_tens_conttens
				local ones_end  = eng_lt20ones
				if tens_cont and ones_end then
					return tens_cont + ones_end -- e.g. ninety-nine -> 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.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)  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 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 = denominatorsdenominator
			if not (dendata and 1  3 do
			if s ~=  then s = s .. ' ' end
			local group_num = math.floor((#num - 1) / 3)
			local group = groupsgroup_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 *>", "") -- 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 = mw.getCurrentFrame():preprocess()
				end
			end
		end
	
		-- Pass args from frame to helper function
		return _numeral_to_english(
			num,
			args'numerator',
			args'denominator',
			args'capitalize',
			args'use_and',
			args'hyphenate',
			args'ordinal',
			args'plural',
			args'links',
			args'negative_word',
			args'round',
			args'zero',
			args'use_one'
		) or 
	end
	
	-- Clone of above for Thai
	local function _numeral_to_thai(num, numerator, denominator, capitalize, use_and, hyphenate, ordinal, _, links, negative_word, round, zero, _)
		if not negative_word then
			negative_word = 'ลบ'
		end
		if negative_word == 'minus' then
			negative_word = 'ลบ'
		end
		local status, fraction_text = fraction_to_thai(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('โหมดการปัดไม่ถูกต้อง: ไม่รู้จัก "' .. round .. '"')
			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("เลขไม่ถูกต้อง") end
		local s = numeral_to_thai(num)
	
		-- For decimal places (if any) output "point" followed by spelling out digit by digit
		if decimal_places then
			s = s .. 'จุด'
			for i = 1, #decimal_places do
				s = s .. thai_ones_positiontonumber(decimal_places:sub(i,i))
			end
		end
	
		s = s:gsub("^%s*(.-)%s*$", "%1")   -- Trim whitespace
		if negative and s ~= zero then s = negative_word .. s end
		s = s:gsub("negative zero", "zero")
		s = s .. fraction_text
		if hyphenate then s = s:gsub("%s", "-") end
		if capitalize then s = s:gsub("^%l", string.upper) end
		if s == 'ลบศูนย์' then s = 'ศูนย์' end
		if s == 'ศูนย์' and zero then s = zero end
		return s
	end
	
	local p = {  -- Functions that can be called from another module
		roman_to_numeral = roman_to_numeral,
		spell_number_eng = _numeral_to_english,
		spell_number2_eng = _numeral_to_english2,	
		english_to_ordinal = english_to_ordinal,
		english_to_numeral = english_to_numeral,
		-- For Thai
		spell_number = _numeral_to_thai
	}
	
	function p._roman_to_numeral(frame) -- Callable via VI
		return roman_to_numeral(frame.args1)
	end
	
	function p._english_to_ordinal(frame) -- callable via First
		return english_to_ordinal(frame.args1)
	end
	
	function p._english_to_numeral(frame) -- callable via One
		return english_to_numeral(frame.args1)
	end
	function p.numeral_to_english(frame)
		local args = frame.args
		local num = args1
		num = num:gsub("^%s*(.-)%s*$", "%1")   -- Trim whitespace
		num = num:gsub(",", "")   -- Remove commas
		num = num:gsub("^*>", "") -- 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()
			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
	
	-- For Thai
	function p.numeral_to_thai(frame)
		local args = frame.args
		local num = args1
		num = num:gsub("^%s*(.-)%s*$", "%1")   -- Trim whitespace
		num = num:gsub(",", "")   -- Remove commas
		num = num:gsub("^*>", "") -- 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()
			end
		end
	
		-- Pass args from frame to helper function
		return _numeral_to_thai(
			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)..digmod+1 else return digmod+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=args1 or pargs1 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