Chuyển số tự nhiên thành dạng viết bằng chữ

Mỗi lần làm hợp đồng là lại phải viết bằng chữ cho số tiền. Cơ bản là số tiền cũng chẵn nên không vấn đề gì, nhưng lỡ gặp số lẻ quá thì nhiều khi cũng mỏi tay.

Chuyển số thành dạng viết bằng chữ là thứ người ta đã làm từ hàng vài trăm tháng trước. Thấy bảo nếu dùng Excel thì lên mạng tải đoạn mã VB gì đấy về cài. Ngoài ra trên mạng cũng có website giúp chuyển đổi trực tuyến, mỗi tội chạy không chuẩn lắm với những số hàng chục nghìn tỉ (chắc chả có mấy ai được tiếp cận đến mức này cả).

Được hôm Paratime Studio rảnh rỗi, anh bèn làm cái này. Mà cũng là để ôn lại Python cho đỡ quên.

import re
import math
import argparse


def in_words_by_level(number, max_level, level = 0):
	"""
		Cách viết bằng chữ của số ở từng hàng
		(đơn vị, chục, trăm, nghìn, triệu, tỉ)
	"""
	digits = ['không', 'một', 'hai', 'ba', 'bốn', 'năm', 'sáu', 'bảy', 'tám', 'chín']

	levels = [
		'',       #0
		'',       #1x
		' trăm',  #2xx
		' nghìn', #3.xxx
		'',       #4x.xxx
		' trăm',  #5x.xxx
		' triệu', #6.xxx.xxx
		'',       #7x.xxx.xxx
		' trăm',  #8xx.xxx.xxx
		' tỉ',    #9.xxx.xxx.xxx
	]

	if level == 0: # hàng đơn vị
		words_ = digits[number]

	elif level in [1, 4, 7]: # hàng chục
		if number > 1:
			words_ = digits[number] + ' mươi'
		elif number == 1:
			words_ = 'mười'
		else:
			words_ = 'linh'

	else: # hàng trăm | nghìn | triệu | tỉ
		words_ = digits[number] + levels[level]

	return words_


def in_words_by_group(chunk, is_last_chunk):
	"""
		Lần lượt ghi bằng chữ cho từng số trong dãy
	"""

	words_ = []

	max_level = len(chunk)

	# Bỏ qua hàng đơn vị nếu thuộc nhóm từ 10 tỉ trở lên
	level = max_level - 1 if is_last_chunk is True else max_level

	for step in range(max_level):
		number = int(chunk[step])
		words_.append(in_words_by_level(number, len(chunk), level - step))

	return words_


def validate_number(number):
	"""
		Kiểm tra tính hợp lệ và định dạng lại số đã nhập
	"""

	# Chuyển số đầu vào sang định dạng chuỗi
	number = str(number).strip()

	# Bỏ các số 0 ở đầu dãy số (nếu có)
	number = re.sub(r'^0+([1-9])', '\\1', number)

	# Dùng cả dấu phảy và dấu chấm
	if re.search('\.', number) and re.search(',', number):
		print('Có thể là số thập phân')
		return False

	# Dùng dấu . hay dấu , làm dấu phân cách?
	separator = None
	for s in ['.', ',']:
		if re.search(f'\\{s}', number):
			separator = s
			break

	# Kiểm tra cấu trúc phân cách hàng nghìn (nếu có)
	if separator is not None:
		chunks = number.split(separator)
		for i, chunk in enumerate(chunks):
			if (i == 0 and len(chunk) > 3) or (i > 0 and len(chunk) != 3):
				print('Kiểm tra lại dấu phân cách hàng nghìn')
				return False

		# Bỏ dấu phân cách
		number = re.sub(f'\\{separator}', '', number)

	# Có phải tất cả đều là chữ số?
	if re.search(r'^[0-9]+$', number) is None:
		print('Số không hợp lệ')
		return False

	return str(number)


def numbers2words(the_number):
	"""
		Chuyển số sang dạng viết bằng chữ
	"""

	the_number = validate_number(the_number)

	if the_number is False:
		return False

	print("{:,}".format(int(the_number)).replace(',', '.'))

	# Trả luôn về "Không" nếu là số 0
	if re.match(r'^0+$', the_number):
		return "Không"

	number_length = len(the_number)

	if number_length > 10: # Số >= 10 tỉ
		chunks = []
		temp = the_number
		is_last = True

		# Chia số thành từng nhóm, từ phải sang trái
		while len(temp) > 0:
			if is_last is True:
				# Nhóm đầu tiên gồm 10 chữ số cuối cùng
				chunks.append(temp[-10:])
				temp = temp[:-10]
			else:
				# Các nhóm còn lại có tối đa 9 chữ số
				chunks.append(temp[-9:])
				temp = temp[:-9]

			is_last = False

		# Đảo thứ tự nhóm về theo thứ tự ban đầu (từ trái sang phái)
		chunks.reverse()
		
		words_ = []
		
		# Lần lượt xử lí từng nhóm
		for chunk in chunks:

			is_last_chunk = True if 10 == len(chunk) else False

			words_ += in_words_by_group(chunk, is_last_chunk)

	else:
		words_ = in_words_by_group(the_number, True)

	# Hiệu chỉnh văn bản đầu ra
	words = ' '.join(words_)

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

	words = re.sub(r'(mười|mươi) không', '\\1', words)
	words = re.sub(r'không trăm linh không (triệu|nghìn)', '', words)
	words = words.replace('không trăm linh không', '')
	words = words.replace('linh không', '')

	words = words.replace('mươi một', 'mươi mốt')
	words = words.replace('mươi bốn', 'mươi tư')
	words = words.replace('mươi năm', 'mươi lăm')

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

	words = re.sub(r' không (trăm|nghìn|triệu|tỉ)\s?$', '', words)

	return words.capitalize()


# Chạy thử
def main():
	# Xử lí tham số đầu vào (qua dòng lệnh)
	ap = argparse.ArgumentParser()
	ap.add_argument('-n', '--number',
        help='Số cần chuyển')
	args = vars(ap.parse_args())

	if args['number']:
		number = args['number']
	else:
		number = input('Mời nhập số: ')

	if len(number) > 0:
		print(numbers2words(number))
	else:
		print("Ơ kìa!")


if __name__ == "__main__":
    main()

Kết quả chạy thử từ dòng lệnh:

> python3 number-to-word.py -n 123.000,000                
Có thể là số thập phân
False

> python3 number-to-word.py -n 123.00.0000
Kiểm tra lại dấu phân cách hàng nghìn
False

> python3 number-to-word.py -n 123.000.000
123.000.000
Một trăm hai mươi ba triệu 

> python3 number-to-word.py -n 012345678909876543210
12.345.678.909.876.543.210
Mười hai tỉ ba trăm bốn mươi lăm triệu sáu trăm bảy mươi tám nghìn chín trăm linh chín tỉ tám trăm bảy mươi sáu triệu năm trăm bốn mươi ba nghìn hai trăm mười

> python3 number-to-word.py -n 34091455901
34.091.455.901
Ba mươi tư tỉ không trăm chín mươi mốt triệu bốn trăm năm mươi lăm nghìn chín trăm linh một

> python3 number-to-word.py -n 345.000.000.000.000.000.001
345.000.000.000.000.000.001
Ba trăm bốn mươi lăm tỉ tỉ không trăm linh một

Chuyên mục:

Một bình luận

  1. […] 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. […]