Vì đã tạo được danh sách tất cả các âm tiết có thể có trong tiếng Việt nên anh đã dùng nó luôn để đối chiếu. Chứ biểu thức chính quy như này trông cực kì cồng kềnh và có vẻ không đáng tin. Nhưng nếu không muốn tải danh sách để đối chiếu (nhất là ở phía front-end) thì regex sẽ là một giải pháp tương đối tiện, dù có thể sẽ nhận nhầm một số trường hợp.
Như thường lệ, anh bắt đầu bằng Python và nếu cần dùng cho giao diện người dùng thì có thể bảo ChatGPT chuyển sang Javascript.
import re
def is_syllable(sample):
sample = sample.strip()
if len(sample) > 7:
return False
# Initials
init = r"(b|c|ch|d|đ|g|gh|gi|h|k|kh|l|m|n|ng|ngh|nh|p|q|ph|r|s|t|th|tr|v|x)"
init_no_q = r"(b|c|ch|d|đ|g|gh|gi|h|k|kh|l|m|n|ng|ngh|nh|p|ph|q|r|s|t|th|tr|v|x)"
# Single nuclears
nuclear0 = r"([aàảãáạăằẳẵắặâầẩẫấậeèẻẽéẹêềểễếệiìỉĩíịoòỏõóọôồổỗốộơờởỡớợuùủũúụưừửữứựyỳỷỹýỵ]|o[oòỏõóọ]|ô[ôồổỗốộ])"
# Dual nuclears
nuclear2 = r"([iìỉĩíịuùủũúụưừửữứự]a)" # ia, ua, ưa
nuclear3 = r"([iy][êềểễếệ]|ư[ơờởỡớợ]|u[ôồổỗốộ])" # iê, yê, ươ, uô
# Tone-restricted nuclei
nuclear4 = r"[aàảãăằẳẵâầẩẫeèẻẽêềểễiìỉĩoòỏõôồổỗơờởỡuùủũưừửữyỳỷỹ]" # Tone 1, 2, 3, 4
# Ending
ending = r"(c|ch|i|m|n|ng|nh|o|p|t|u|y)"
ending_no_y = r"(c|ch|i|m|n|ng|nh|o|p|t|u)"
# Valid syllables
x110 = fr"(qu[aàảãáạ]|{init}?(o[aàảãáạeèẻẽéẹ]|u[eèẻẽéẹêềểễếệơờởỡớợyỳỷỹýỵ]|u[yỳỷỹýỵ]a))"
x010 = fr"({init_no_q}?({nuclear0}|{nuclear2}))"
x011 = fr"({init}?({nuclear0}|{nuclear3}|o[oòóỏõóọ]){ending}|[uùủũúụ]{ending_no_y})"
x111 = fr"({init}?((o[aàảãáạăằẳẵắặeèẻẽéẹ]|u[ăằẳẵắặâầẩẫấậeèẻẽéẹêềểễếệơờởỡớợyỳỷỹýỵ])|uy[aêềểễếệ]|qu[aàảãáạ]){ending})"
comp = fr"({x110}|{x010}|{x011}|{x111})"
# Invalid syllables
not1 = r"^((k|gh|ngh)[^heèẻẽéẹêềểễếệiìỉĩíị]|q[^u]).*$"
not2 = (
fr"(hh|{nuclear4}[ptc]"
fr"|[eèẻẽéẹ](i|nh|u|y)"
fr"|[^g][iìỉĩíị](i|o|y)"
fr"|[oòỏõóọ](nh|u|y)"
fr"|[uùủũúụ](ch|nh|o|u))"
)
not3 = r"(c|ng)[eèẻẽéẹêềểễếệiìỉĩíị]|g[eèẻẽéẹêềểễếệ]|gi[iìỉĩíị]"
# Final checks
if re.match(fr"^{comp}$", sample, re.IGNORECASE) \
and not re.match(not1, sample, re.IGNORECASE) \
and not re.match(fr"(.*)?{not2}", sample, re.IGNORECASE) \
and not re.match(fr"{not3}", sample, re.IGNORECASE):
return True
return False
# Example usage
s = 'nguyêng'
print(f"{s} is {'valid' if is_syllable(s) else 'NOT valid'}")