Wiki - KEONHACAI COPA

Mô đun:ConvertNumeric

Tài liệu mô đun[xem] [sửa] [lịch sử] [làm mới]

Mô đun này mở rộng số thành cụm từ tiếng Việt hoặc tiếng Anh.

Xem thêm[sửa mã nguồn]

-- 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_numerals[c]
		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 < 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 numeral_to_vietnamese_less_100(num, ordinal, plural, zero)
	-- TODO: Chục
	local terminal_ones
	if ordinal then
		terminal_ones = ones_position_ord_vi
	else
		terminal_ones = ones_position_vi
	end

	if num == 0 and zero ~= nil then
		return zero
	elseif num <= 10 then
		return terminal_ones[num]
	elseif num % 10 == 0 then
		return terminal_ones[num / 10] .. ' mươi'
	else
		local ten = math.floor(num / 10)
		local terminal_one = terminal_ones[num % 10]
		local terminal_ten = ones_position_vi[ten] .. ' mươi'
		if ten == 1 then
			terminal_ten = terminal_ones[10]
		elseif num % 10 == 1 then
			terminal_one = "mốt"
		end
		if num % 10 == 5 then terminal_one = "lăm" end
		return terminal_ten .. ' ' .. terminal_one
	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

local function numeral_to_vietnamese_less_1000(num, use_and, ordinal, plural, zero, half)
	num = tonumber(num)
	if num < 100 then
		return numeral_to_vietnamese_less_100(num, ordinal, plural, zero)
	elseif num % 100 == 0 then
		return ones_position_vi[num/100] .. ' trăm'
	elseif half and num % 100 == 50 then
		return ones_position_vi[math.floor(num/100)] .. ' trăm rưỡi'
	elseif num % 100 <= 10 then
		local terminal_ones
		if ordinal then
			terminal_ones = ones_position_ord_vi
		else
			terminal_ones = ones_position_vi
		end
		return ones_position_vi[math.floor(num/100)] .. ' trăm lẻ ' .. terminal_ones[num % 100]	-- linh
	else
		return ones_position_vi[math.floor(num/100)] .. ' trăm ' .. numeral_to_vietnamese_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

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 = 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 = 'one'
			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

local function fraction_to_vietnamese(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)
		if numerator > 0 and numerator * 2 == denominator then
			return 'ok', ' rưỡi'
		end
		if not (1 <= numerator and numerator <= 999 and denominator <= 999) then
			return 'unsupported', ''
		end
		local numstr = numeral_to_vietnamese_less_1000(numerator)
		local denstr = numeral_to_vietnamese_less_1000(denominator, false, true)
		denstr = denstr:gsub("^một ", "")
		if finished then
			return 'finished', sign .. numstr .. ' phần ' .. denstr
		end
		return 'ok', ' ' .. numstr .. ' phần ' .. 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 "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'; 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)
	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
			return 'Chế độ làm tròn không hợp lệ'
		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 [[Mô đun: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 return 'Chữ số thập phân không hợp lệ' 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 .. '[[Bậc độ lớn (số)#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")
	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 function _numeral_to_vietnamese(num, numerator, denominator, capitalize, use_and, hyphenate, ordinal, plural, links, negative_word, round, zero, use_one)
	if not negative_word then
		negative_word = 'âm'
	end
	local status, fraction_text = fraction_to_vietnamese(num, numerator, denominator, not use_and, negative_word)
	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 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
			return 'Chế độ làm tròn không hợp lệ'
		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 [[Mô đun: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 return 'Chữ số thập phân không hợp lệ' end
	
	local entire_num = num

	-- 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_vi[group_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 <= 13) then
				s = s .. '[[Bậc độ lớn (số)#10' .. group_num*3 .. '|' .. group .. ']]'
			else
				s = s .. group
			end
		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
		s = s .. ' '
	end
	if tonumber(entire_num) > 500 and tonumber(num) == 500 then
		s = s .. "rưỡi"
	elseif s == '' or num ~= '' then
		if tonumber(entire_num) > 1000 and tonumber(num) <= 10 then
			s = s .. "lẻ "
		end
		s = s .. numeral_to_vietnamese_less_1000(num, use_and, ordinal, plural, zero, #entire_num <= 3)
	end

	-- For decimal places (if any) output "point" followed by spelling out digit by digit
	if decimal_places then
		s = s .. ' phẩy'
		for i = 1, #decimal_places do
			s = s .. ' ' .. ones_position_vi[tonumber(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("âm không", "không")
	s = s .. fraction_text
	if capitalize then s = s:gsub("^%l", string.upper) end
	return s
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_lt20[v] = k
	end
	local vie_lt20_special = vie_lt20
	vie_lt20_special['mốt'] = 1

	if vie_lt20[vie] then
		return vie_lt20[vie] -- e.g. một -> 1
	elseif vie_tens_cont[vie] then
		return vie_tens_cont[vie] -- 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_cont[tens]
			local ones_end  = vie_lt20_special[ones]
			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,
	vietnamese_to_numeral = vietnamese_to_numeral,
}

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
	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

function p.numeral_to_vietnamese(frame)
	local args = frame.args
	local num = args[1]
	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('{{#expr: ' .. num .. '}}')
		end
	end

	-- Pass args from frame to helper function
	local numerator = mw.text.trim(args['numerator'] or args[2])
	if #numerator == 0 then numerator = nil end
	local denominator = mw.text.trim(args['denominator'] or args[3])
	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)..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

--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
Wiki - Keonhacai copa chuyên cung cấp kiến thức thể thao, keonhacai tỷ lệ kèo, bóng đá, khoa học, kiến thức hằng ngày được chúng tôi cập nhật mỗi ngày mà bạn có thể tìm kiếm tại đây có nguồn bài viết: https://vi.wikipedia.org/wiki/M%C3%B4_%C4%91un:ConvertNumeric