Mô đun:ConvertNumeric

-- Mô đun để chuyển đổi qua lại những cách trình bày số. Xem tài liệu tại trang thảo luận.
-- Có sẵn trường hợp thử thách tại: Mô đun:ConvertNumeric/testcases
-- Sử dụng trang này để xem trước khi sửa đổi: Thảo luận Mô đun:ConvertNumeric/testcases
-- Trước tiên sửa đổi Mô đun:ConvertNumeric/sandbox rồi xem trước dùng Thảo luận Mô đun: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_vi = {
	0 = 'không',
	1 = 'một',
	2 = 'hai',
	3 = 'ba',
	4 = 'bốn',
	5 = 'năm',
	6 = 'sáu',
	7 = 'bảy',
	8 = 'tám',
	9 = 'chín',
	10 = 'mười',
	11 = 'mười một',
	12 = 'mười hai',
	13 = 'mười ba',
	14 = 'mười bốn',
	15 = 'mười lăm',
	16 = 'mười sáu',
	17 = 'mười bảy',
	18 = 'mười tám',
	19 = 'mười chín'
}
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_ord_vi = {
	0 = 'không',
	1 = 'nhất',
	2 = 'hai',	-- nhì
	3 = 'ba',
	4 = 'tư',
	5 = 'năm',
	6 = 'sáu',
	7 = 'bảy',
	8 = 'tám',
	9 = 'chín',
	10 = 'mười',
	11 = 'mười một',
	12 = 'mười hai',
	13 = 'mười ba',
	14 = 'mười bốn',
	15 = 'mười lăm',
	16 = 'mười sáu',
	17 = 'mười bảy',
	18 = 'mười tám',
	19 = 'mười chín'
}
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_vi = {
	2 = 'hai mươi',
	3 = 'ba mươi',
	4 = 'bốn mươi',
	5 = 'năm mươi',
	6 = 'sáu mươi',
	7 = 'bảy mươi',
	8 = 'tám mươi',
	9 = 'chín mươi'
}
local tens_position_ord = {
	2 = 'twentieth',
	3 = 'thirtieth',
	4 = 'fortieth',
	5 = 'fiftieth',
	6 = 'sixtieth',
	7 = 'seventieth',
	8 = 'eightieth',
	9 = 'ninetieth'
}
local tens_position_ord_vi = {
	2 = 'hai mươi',
	3 = 'ba mươi',
	4 = 'bốn mươi',
	5 = 'năm mươi',
	6 = 'sáu mươi',
	7 = 'bảy mươi',
	8 = 'tám mươi',
	9 = 'chín mươi'
}
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 groups_vi = {
	1 = 'ngàn',	-- nghìn
	2 = 'triệu',
	3 = 'tỷ',	-- tỉ
	4 = 'ngàn tỷ',
	5 = 'triệu tỷ',
	6 = 'tỷ tỷ',
	7 = 'ngàn tỷ tỷ',
	8 = 'triệu tỷ tỷ',
	9 = 'tỷ tỷ tỷ',
	10 = 'ngàn tỷ tỷ tỷ',
	11 = 'triệu tỷ tỷ tỷ',
	12 = 'tỷ tỷ tỷ tỷ',
	13 = 'ngàn tỷ tỷ tỷ tỷ',
	14 = 'triệu tỷ tỷ tỷ tỷ',
}
local vie_tens_cont = {
	'hai mươi'	= 20,
	'ba mươi'	    = 30,
	'bôn mươi'	= 40,
	'năm mươi'	= 50,
	'sáu mươi'	= 60,
	'bảy mươi'	= 70,
	'tám mươi'	= 80,
	'chín mươi'	= 90,
}
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, "số La Mã không phải là chuỗi" 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, "số La Mã có ký tự không hợp lệ " .. 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  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.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)  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 = { 'half', plural = 'halves' },
	3 = { 'third' },
	4 = { 'quarter', us = 'fourth' },
	5 = { 'fifth' },
	6 = { 'sixth' },
	8 = { 'eighth' },
	10 = { 'tenth' },
	16 = { 'sixteenth' },
}
-- 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 = denominatorsdenominator
		if not (dendata and 1  0 and numerator * 2 == denominator then
			return 'ok', ' rưỡi'
		end
		if not (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  3 do
		if s ~=  then s = s .. ' ' end
		local group_num = math.floor((#num - 1) / 3)
		local group = groups_vigroup_num
		local group_digits = #num - group_num*3
		if tonumber(num:gsub("^500", ""), 10)  == 0 and tonumber(num:gsub("^1000", ""), 10) == 0 then
			s = s .. "rưỡi"
		else
			s = s .. numeral_to_vietnamese_less_1000(num:sub(1, group_digits), false, false, false, zero, false) .. ' '
			if links and (((links == 'on' and group_num >= 3) or links:find(group)) and group_num  500 and tonumber(num) == 500 then
		s = s .. "rưỡi"
	elseif s ==  or num ~=  then
		if tonumber(entire_num) > 1000 and tonumber(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 eval it as an expr to see
			-- if that produces a number (e.g. "3 + 5" will become "8").
			local noerr, result = pcall(mw.ext.ParserFunctions.expr, num)
			if noerr then
				num = result
			end
		end
	end
	-- Call helper function passing args
	return _numeral_to_vietnamese(
		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
local function vietnamese_to_numeral(vietnamese)
	local vie = string.lower(vietnamese or )
	local vie_lt20 = {} -- hoán đổi khoá và giá trị của ones_position_vi{}
	for k, v in pairs( ones_position_vi ) do
		vie_lt20v = k
	end
	local vie_lt20_special = vie_lt20
	vie_lt20_special'mốt' = 1
	if vie_lt20vie then
		return vie_lt20vie -- e.g. một -> 1
	elseif vie_tens_contvie then
		return vie_tens_contvie -- e.g. chín mươi -> 90
	else
		local tens, ones = string.match(vie, '^(.+%s.+)%s+(.+)$')
		if tens and ones then
			local tens_cont = vie_tens_conttens
			local ones_end  = vie_lt20_specialones
			if tens_cont and ones_end then
				return tens_cont + ones_end -- e.g. chín mươi chín -> 99
			end
		end
	end
	return -1 -- Failed
end
local p = {	-- functions that can be called from another module
	roman_to_numeral = roman_to_numeral,
	spell_number = _numeral_to_vietnamese,
	spell_number2 = _numeral_to_vietnamese2,
	vietnamese_to_numeral = vietnamese_to_numeral,
}
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
	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
function p.numeral_to_vietnamese(frame)
	local args = frame.args
	local num = args1
	num = mw.text.trim(num)   -- Trim whitespace
	num = num:gsub("%.", ""):gsub(",", ".")   -- Convert to computer format
	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
	local numerator = mw.text.trim(args'numerator' or args2)
	if #numerator == 0 then numerator = nil end
	local denominator = mw.text.trim(args'denominator' or args3)
	if #denominator == 0 then denominator = nil end
	local case = args'case'
	local sp = args'sp'
	local adj = args'adj'
	local ord = args'ord'
	local pl = args'pl'
	local lk = args'lk'
	local negative = args'negative'
	local round = args'round'
	local zero = args'zero'
	return _numeral_to_vietnamese(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)..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
--p.numeral_to_vietnamese_less_100 = numeral_to_vietnamese_less_100				-- debug
--p.numeral_to_vietnamese_less_1000 = numeral_to_vietnamese_less_1000				-- debug
return p