Chuyển từ viết bằng chữ sang số

Chuyển từ số sang chữ cũng không phức tạp lắm. Mò mẫm nửa ngày là xong. Nhưng ngược lại thì mất cả ngày liền.

Từ số sang chữ dễ hơn một phần là bởi số là thứ phải viết chính xác, gần như là duy nhất. Còn ở chiều ngược lại, khi diễn đạt bằng lời thì lại có nhiều cách nhau, cả về cấu trúc lẫn từ ngữ.

Bằng một nỗ lực nho nhỏ, anh cố gắng nhận diện cả cách nói tắt kiểu như trăm rưỡi, nghìn mốt, triệu chín năm chục; các biến thể phát âm như hai lăm, băm nhăm; hay một số từ lóng như củ, chai, lít v.v.

Và giải pháp vẫn là quy chuẩn từ ngữ và cấu trúc trước đưa vào phân tích, hạn chế tối đa những ngoại lệ ngoắt ngoéo.

import re
from pprint import pprint

NUMBERS_IN_WORDS = {
	'không': '0',
	'linh':  '0',
	'mười':  '1',
	'một':   '1',
	'hai':   '2',
	'ba':    '3',
	'bốn':   '4',
	'năm':   '5',
	'sáu':   '6',
	'bảy':   '7',
	'tám':   '8',
	'chín':  '9',
}

LEVELS = {
	'tỉ':    '000000000',
	'triệu': '000000',
	'nghìn': '000',
	'trăm':  '00',
}

SHORTEN = {
	'tỉ':    'trăm triệu',
	'triệu': 'trăm nghìn',
	'nghìn': 'trăm',
	'trăm':  'mươi',
}

STEPS = ['tỉ', 'triệu', 'nghìn', 'trăm']

VALIDS = ['mươi']
for k in NUMBERS_IN_WORDS.keys():
	VALIDS.append(k)
for k in LEVELS.keys():
	VALIDS.append(k)

NUMBERS_PATTERN = '|'.join(NUMBERS_IN_WORDS.keys())

def validate_words(words):

	words = re.sub(r'\s+', ' ', words)

	words = words.strip().lower()

	# thêm 'một ' vào đầu chuỗi nếu nói tắt với hàng đặc biệt
	if re.search(r'^(vạn|xịch|cành|lít|loét)\b', words):
		words = 'một ' + words

	synonyms = {
		'lẻ':   'linh',
		'mốt':  'một',
		'(hăm( mươi)?)':  'hai mươi',
		'(băm( mươi)?)':  'ba mươi',
		'tư':   'bốn',
		'dăm':  'năm',
		'lăm':  'năm',
		'nhăm': 'năm',
		'rưởi': 'năm',
		'rưỡi': 'năm',
		'bẩy':  'bảy',
		'chục': 'mươi',
		'ngàn': 'nghìn',
		'xịch': 'mươi nghìn',
		'chai': 'triệu',
		'củ':   'triệu',
		'tỏi':  'tỉ',
		'tỷ':   'tỉ',
	}

	for i, j in synonyms.items():
		words = re.sub(rf'\b{i}\b', f'{j}', words)

	# thêm 'một ' vào đầu chuỗi nếu nói tắt (tỉ rưỡi)
	if re.search(r'^(tỉ|triệu|nghìn|trăm)\b', words):
		words = 'một ' + words

	# nói tắt kiểu 'triệu chín năm chục'
	for i, l in enumerate(STEPS[:-1]):
		if re.search(rf'{l} ({NUMBERS_PATTERN}) ({NUMBERS_PATTERN}) mươi$', words):
			stripped = STEPS[i+1]
			words = re.sub(rf'({l}) ({NUMBERS_PATTERN}) ({NUMBERS_PATTERN}) mươi$', f'\\1 \\2 trăm \\3 mươi {stripped}', words)
			break

	# nói tắt phần sau, kiểu 'hai tỉ hai trăm'
	for i, l in enumerate(STEPS[:-2]):
		stripped = STEPS[i+1]
		if re.search(rf'{l} ({NUMBERS_PATTERN}) trăm$', words):
			words = words + ' ' + stripped
			break

		if re.search(rf'{l} ({NUMBERS_PATTERN}) mươi$', words):
			words = re.sub(rf'({l}) ({NUMBERS_PATTERN}) mươi$', f'\\1 không trăm \\2 mươi {stripped}', words)
			break

	# nói tắt phần sau, kiểu 'hai tỉ hai'
	for level, stripped in SHORTEN.items():
		if re.search(rf'{level} ({NUMBERS_PATTERN})$', words):
			words = words + ' ' + stripped
			break

	if re.search(r'\b(cành|lít|loét)\b', words):
		words = re.sub(rf'(cành|lít|loét) ({NUMBERS_PATTERN})?$', f'trăm \\2 mươi nghìn', words)
		words = re.sub(rf'(cành|lít|loét)$', f'trăm nghìn', words)

	# ba vạn chín nghìn
	if re.search(r'\bvạn\b', words):
		words = re.sub(rf'({NUMBERS_PATTERN}) vạn\s?({NUMBERS_PATTERN})?( nghìn)?', '\\1 mươi \\2 nghìn', words)

	# tìm từ không hợp lệ
	words = re.sub(r'\s+', ' ', words.strip())
	for w in words.split(' '):
		if w not in VALIDS:
			print('Không hợp lệ:', w)
			words = ''
			break

	print('Validated to:', words)
	return words

#print(validate_words('hai lẻ mốt'))


def units_of(words):
	words = words.strip()

	if words != 'mười':
		if re.search(rf'^({NUMBERS_PATTERN})$', words):
			return NUMBERS_IN_WORDS[words]

	else:
		return '10'
	
	return False

#for w in ['một', 'chính', 'mười', 'tám']:
#	print(units_of(w), '-', w)


def under_hundreds(words):
	words = words.strip()

	if len(words) == 0:
		return '0'

	if words == 'mười':
		return '10'

	s = re.findall(rf'^({NUMBERS_PATTERN}) (mươi)?\s?({NUMBERS_PATTERN})?$', words)
	if s:

		_tens = s[0][0]
		_units = s[0][2]
		tens = NUMBERS_IN_WORDS[_tens]
		units = NUMBERS_IN_WORDS[_units] if len(_units) else '0'

		return tens + units

	if re.search(rf'^({NUMBERS_PATTERN})$', words):
		return NUMBERS_IN_WORDS[words]
	
	print('LỖI (hàng chục/đơn vị): ', words)
	
	return False

#for w in ['chính tám', 'tám mươi chín', 'bảy chục', 'linh hai', 'mười một', 'mười', 'bảy', 'không']:
#	print(under_hundreds(w), '-', w)


def hundreds_of(words):

	words = words.strip()

	s = re.findall(rf'\b({NUMBERS_PATTERN}) (trăm)\s?(.*)?$', words)
	if s:
		#pprint(s)
		_hundreds = s[0][0]
		_tens = s[0][2]
		hundreds = NUMBERS_IN_WORDS[_hundreds]
		tens = under_hundreds(_tens) if len(_tens) else '00'

		if tens is False:
			return False

		return hundreds + tens

	else:
		return under_hundreds(words)


#for w in ['không trăm hai mươi', 'tám trăm linh chín', 'bảy trăm mười', 'sáu trăm', '']:
#	print(hundreds_of(w), '-', w)


def above_hundreds(words: str, level: str, NUMBER):

	words = words.strip()

	s = re.findall(rf'^(.*)\s?{level}\s?(.*)?$', words)

	if s:

		_head = hundreds_of(s[0][0])
		if _head is False:
			_head = under_hundreds(s[0][0])
		
		head = _head if _head else ''

		num = head + LEVELS[level]

		NUMBER.append(int(num))

		words = s[0][1]

	return words


def group_of_nine(words):

	words = words.strip()

	NUMBER = []

	words = above_hundreds(words, 'triệu', NUMBER)

	words = above_hundreds(words, 'nghìn', NUMBER)

	_hundreds = hundreds_of(words)

	if _hundreds is False:
		return False

	if _hundreds:
		NUMBER.append(int(_hundreds))

	the_number = 0
	for n in NUMBER:
		the_number += n

	length = len(str(the_number))

	# Định dạng lại cho đủ 9 chữ số
	return (9 - length) * '0' + str(the_number)


def words2numbers(words):

	words = validate_words(words)

	if len(words) == 0:
		return False

	# Có thể là đọc từng số
	if re.search(r'\b(mươi|trăm|nghìn|triệu|tỉ)\b', words) is None:
		print('Có thể là đang đọc từng chữ số riêng lẻ')
		numbers = []
		all_words = words.split(' ')
		for word in all_words:
			if word in NUMBERS_IN_WORDS.keys():
				numbers.append(units_of(word))
			else:
				print('Số không hợp lệ', word)
				return False

		return ''.join(numbers)


	chunks = words.split('tỉ')

	#pprint(chunks)

	number = False

	if len(chunks):

		parts = []
		for i, chunk in enumerate(chunks):
			chunk = chunk.strip()

			if len(chunk) == 0:
				#print('Nothing')
				parts.append(9 * '0')
				continue

			_number = group_of_nine(chunk.strip())

			parts.append(_number)

		if _number:
			number = int(''.join(parts))

	else:
		number = group_of_nine(words)

	return number

Chạy thử:

# Test
test = [
	'hai trăm mười một triệu ba trăm sáu mươi nhăm nghìn bốn trăm mười một tỉ năm trăm mười lăm triệu sáu trăm hai tư nghìn bảy trăm hăm mốt',
	'một tỉ năm trăm triệu hai trăm linh chín nghìn',
	'một tỉ năm trăm triệu hai trăm linh chín',
	'bảy tỉ không trăm linh hai triệu',
	'hai tỉ không nghìn ba trăm',
	'hăm lăm',
	'băm mươi lăm',
	'chín chục',
	'trăm rưởi',
	'nghìn chín',
	'vạn',
	'chín vạn rưởi',
	'ba vạn chín nghìn',
	'ba vạn chín nghìn năm trăm',
	'tỉ tư',
	'một củ',
	'tám triệu hai',
	'triệu bảy năm chục',
	'năm cành',
	'năm lít rưỡi',
	'lít tư',
	'loét mốt',
	'một chín',
	'không tám',
	'một chín không hai',
	'năm trăm mươi'
]

for w in test:
	print('Input:', w)
	number = words2numbers(w)

	if number:
		if isinstance(number, int):
			print("{:,}".format(number).replace(',', '.'))
		else:
			print(number)
	else:
		print('LỖI: Không thể chuyển đổi')

	print(20 * '-')

Cho ra kết quả:

Input: hai trăm mười một triệu ba trăm sáu mươi nhăm nghìn bốn trăm mười một tỉ năm trăm mười lăm triệu sáu trăm hai tư nghìn bảy trăm hăm mốt
Validated to: hai trăm mười một triệu ba trăm sáu mươi năm nghìn bốn trăm mười một tỉ năm trăm mười năm triệu sáu trăm hai bốn nghìn bảy trăm hai mươi một
211.365.411.515.624.721
--------------------
Input: một tỉ năm trăm triệu hai trăm linh chín nghìn
Validated to: một tỉ năm trăm triệu hai trăm linh chín nghìn
1.500.209.000
--------------------
Input: một tỉ năm trăm triệu hai trăm linh chín
Validated to: một tỉ năm trăm triệu hai trăm linh chín
1.500.000.209
--------------------
Input: bảy tỉ không trăm linh hai triệu
Validated to: bảy tỉ không trăm linh hai triệu
7.002.000.000
--------------------
Input: hai tỉ không nghìn ba trăm
Validated to: hai tỉ không nghìn ba trăm
2.000.000.300
--------------------
Input: hăm lăm
Validated to: hai mươi năm
25
--------------------
Input: băm mươi lăm
Validated to: ba mươi năm
35
--------------------
Input: chín chục
Validated to: chín mươi
90
--------------------
Input: trăm rưởi
Validated to: một trăm năm mươi
150
--------------------
Input: nghìn chín
Validated to: một nghìn chín trăm
1.900
--------------------
Input: vạn
Validated to: một mươi nghìn
10.000
--------------------
Input: chín vạn rưởi
Validated to: chín mươi năm nghìn
95.000
--------------------
Input: ba vạn chín nghìn
Validated to: ba mươi chín nghìn
39.000
--------------------
Input: ba vạn chín nghìn năm trăm
Validated to: ba mươi chín nghìn năm trăm
39.500
--------------------
Input: tỉ tư
Validated to: một tỉ bốn trăm triệu
1.400.000.000
--------------------
Input: một củ
Validated to: một triệu
1.000.000
--------------------
Input: tám triệu hai
Validated to: tám triệu hai trăm nghìn
8.200.000
--------------------
Input: triệu bảy năm chục
Validated to: một triệu bảy trăm năm mươi nghìn
1.750.000
--------------------
Input: năm cành
Validated to: năm trăm nghìn
500.000
--------------------
Input: năm lít rưỡi
Validated to: năm trăm năm mươi nghìn
550.000
--------------------
Input: lít tư
Validated to: một trăm bốn mươi nghìn
140.000
--------------------
Input: loét mốt
Validated to: một trăm một mươi nghìn
110.000
--------------------
Input: một chín
Validated to: một chín
Có thể là đang đọc từng chữ số riêng lẻ
19
--------------------
Input: không tám
Validated to: không tám
Có thể là đang đọc từng chữ số riêng lẻ
08
--------------------
Input: một chín không hai
Validated to: một chín không hai
Có thể là đang đọc từng chữ số riêng lẻ
1902
--------------------
Input: năm trăm mươi
Validated to: năm trăm mươi
LỖI (hàng chục/đơn vị):  mươi
LỖI: Không thể chuyển đổi

Tiện tay anh cho luôn vào Dự án S. Nhưng vẫn đang phải xử lí bằng Python chứ chuyển sang JavaScript thì ngại quá.


Chuyên mục: