2026-03-05|12 min read|--lakinh--dev-log--debugging--vietnamese-astrology--tuvi

When Stars Lie: Debugging a 2,000-Year-Old Algorithm

I've been building LaKinh — a Vietnamese Tử Vi astrology app — for a few months now. The engine has gone through a complete multi-school refactor, dozens of star implementations, and more verification sessions than I can count.

But last night reminded me: bugs in this domain don't announce themselves. They just quietly produce the wrong destiny for your users.


## The Problem With Debugging Astrology

Normal debugging: you have a spec, you have a test, you know what "correct" means.

Tử Vi debugging: you have five different websites. They all claim to be correct. Three of them agree with each other. One of them is yours. None of them show their source code.

This is the world I work in.

When I noticed LaKinh was producing wrong results after the multi-school refactor, I started systematic cross-referencing. I'd pick a birth date, run it through LaKinh, then check tuvivietnam.vn, kabala.vn, tuvi.bio, and sometimes lichngaytot.com. Then I'd sit there staring at five different charts, trying to figure out which one was right.


## Test Case 1: My Own Birth Chart

I started with what I knew best — my own birth data. Bính Dần year, 5th day of the 5th lunar month, born at Ngọ hour, Male.

LaKinh said Thiên Cơ at my Mệnh palace.
Four other sites said Phá Quân.

That's not a small difference. Thiên Cơ and Phá Quân are completely different stars with completely different life readings. If your app tells someone they're a Thiên Cơ person when they're actually Phá Quân — that's not a minor glitch. That's the app lying about who someone is.

I went looking for the bug.


## The Tử Vi Star Algorithm

The main star placement in Tử Vi (called an Tử Vi in Vietnamese) works like this: you take the lunar day, divide it by the Cục value (a number between 2 and 6 representing your elemental group), and use the quotient to determine where the Tử Vi star sits in the 12-palace chart.

LaKinh's original formula:

const thuong = Math.floor(lunarDay / cucValue);
const du = lunarDay % cucValue;

if (du === 0) {
  index = thuong % 12;
} else {
  index = (thuong + cucValue - du) % 12;
}

Simple. Clean. Wrong.

The actual algorithm — which I found by digging through the source code of iztro, a Chinese open-source Tử Vi library — uses a loop:

do {
  offset++;
  const divisor = lunarDay + offset;
  quotient = Math.floor(divisor / cucValue);
  remainder = divisor % cucValue;
} while (remainder !== 0);

You increment an offset until you find a number that divides evenly. Then you use the parity of the offset (even vs odd) to adjust the final position — adding if even, subtracting if odd.

My formula was missing the entire offset loop. It worked correctly when the lunar day divided evenly by the Cục value with no remainder. For every other case, it produced a wrong answer. Since most days don't divide cleanly, most charts were wrong.

I verified the fix across 3 test cases. All passed.


## Test Case 4: A 1990 Birth Chart

After fixing the main star bug, I ran a second test case with different data to verify. Canh Ngọ year, 13th day of the 6th lunar month, Thìn hour, Female.

Main stars: all 14 matched perfectly across LaKinh, tuvivietnam.vn, and kabala.vn. ✅

Then I noticed something in the secondary stars. The Tứ Hóa table — which assigns transformation types (Lộc, Quyền, Khoa, Kỵ) to specific stars based on the year — had an error.

For year Canh, LaKinh's Trung Châu school had:

'Canh': { loc: 'Thái Dương', quyen: 'Vũ Khúc', khoa: 'Thái Dương', ky: 'Thiên Đồng' }

Notice anything? loc and khoa both point to Thái Dương. That means Thái Dương gets the Lộc marker, and Hóa Khoa essentially disappears — there's nowhere to display it when the same star already has a different transformation.

Both tuvivietnam.vn and kabala.vn showed Hóa Khoa at Thái Âm for year Canh. One line fix:

'Canh': { loc: 'Thái Dương', quyen: 'Vũ Khúc', khoa: 'Thái Âm', ky: 'Thiên Đồng' }

## Test Case 5: A 1975 Birth Chart

Then came the third bug — the one I didn't expect at all.

I tried to enter a 1975 birth date into LaKinh. Selected 15/03/1975. Pressed confirm. The app displayed 14/03/1975.

The date was wrong by one day. Only for pre-1975 dates.

After some digging, I found the root cause: Vietnam used UTC+8 before June 1975 (when the country unified and standardized to UTC+7). iOS knows this historical timezone data. So when you select a date in 1975, the DateTimePicker returns a Date object with a UTC+8 offset. When that gets stored and later parsed as new Date("1975-03-15") — which JavaScript interprets as UTC midnight — the local display shifts back one day.

Fix: parse the stored date string by extracting components directly instead of letting JavaScript's Date constructor apply timezone conversion:

// Before (wrong):
setBirthDate(new Date(profile.birthDate));

// After (correct):
const [y, m, d] = profile.birthDate.split('-').map(Number);
setBirthDate(new Date(y, m - 1, d));

But there was a deeper problem. The lunar calendar conversion function itself was also using local Date objects for offset calculation:

// Before:
const solarDate = new Date(year, month - 1, day);
let offset = Math.floor((solarDate.getTime() - BASE_DATE.getTime()) / 86400000);

When two Date objects have different historical timezone offsets, subtracting their .getTime() values introduces error. For pre-1975 dates, the offset was off by roughly 19 days — which is why a March 1975 date was converting to the wrong lunar date entirely.

Fix: use Date.UTC() for pure timezone-agnostic arithmetic:

// After:
const BASE_DAYS = Date.UTC(1900, 0, 31) / 86400000;
function toUTCDays(y: number, m: number, d: number) {
  return Date.UTC(y, m - 1, d) / 86400000;
}
let offset = toUTCDays(year, month, day) - BASE_DAYS;

After the fix, 15/03/1975 correctly converts to day 3 of month 2 in the Ất Mão year — matching both tuvivietnam.vn and kabala.vn exactly.


## The tuviglobal Problem

One thing I haven't mentioned: not all sources are equal.

During testing, I also checked tuviglobal.com — and it was consistently different from tuvivietnam.vn and kabala.vn. Different main star positions, different secondary star placements.

After some research, I realized tuviglobal.com appears to follow a Chinese Tử Vi interpretation — which shares historical roots with Vietnamese Tử Vi but diverges significantly on several formulas. The Vietnamese tradition (as practiced by tuvivietnam.vn and kabala.vn) has its own established standards.

This is a real problem when you're building an app for Vietnamese users. If you use the wrong source as your reference, you can end up with an engine that's technically "based on Tử Vi" but produces charts that Vietnamese practitioners would immediately recognize as wrong.

I decided: when in doubt, follow tuvivietnam.vn and kabala.vn. They're the most widely used Vietnamese Tử Vi references, and when they agree with each other, that's as close to a ground truth as I'm going to get.


## What I Verified

After all three fixes:

  • >Test Case 1 (my birth data): 14/14 main stars correct ✅
  • >Test Case 2 (1986, Female): 14/14 correct ✅
  • >Test Case 3 (1986, Male): 14/14 correct ✅
  • >Test Case 4 (1990, Female): 14/14 correct, Tứ Hóa now displaying ✅
  • >Test Case 5 (1975, Male): 14/14 correct, lunar date conversion correct ✅

Three bugs. Five test cases. Zero remaining discrepancies with tuvivietnam.vn and kabala.vn.


## The Part Nobody Tells You About Domain Apps

Building a calculator app is simple: 2 + 2 = 4, no debate.

Building a domain-specific app with 2,000 years of tradition, multiple competing schools, and zero formal specification? Every single formula needs to be verified against real-world references. And those references sometimes disagree with each other.

The thing that kept me sane: cross-referencing multiple sources, picking the majority consensus, and being systematic about it. When tuvivietnam.vn and kabala.vn both agree on something, I trust it. When only one site shows something unusual, I investigate before changing code.

It's slow. It's tedious. But it's the only way to build something that Vietnamese users will actually trust.


LaKinh is a Vietnamese Tử Vi app I'm building solo. Follow the journey:



## Khi Sao Nói Dối: Debug Một Thuật Toán 2.000 Năm Tuổi

Tôi đang xây dựng LaKinh — một ứng dụng Tử Vi Việt Nam. Engine đã trải qua refactor đa trường phái, hàng chục lần an sao, và vô số buổi kiểm tra chéo.

Nhưng tối qua nhắc tôi nhớ: bug trong lĩnh vực này không tự thông báo. Chúng chỉ lặng lẽ tạo ra một số mệnh sai cho người dùng.


### Vấn Đề Với Debug Tử Vi

Debug thông thường: có spec, có test, biết "đúng" nghĩa là gì.

Debug Tử Vi: có năm website khác nhau. Tất cả đều tự nhận là đúng. Ba cái đồng ý với nhau. Một cái là của mình. Không cái nào cho xem source code.

Đây là thế giới tôi đang làm việc.


### Test Case 1: Lá Số Của Chính Tôi

Tôi bắt đầu từ dữ liệu mình hiểu rõ nhất — ngày sinh của bản thân. Năm Bính Dần, ngày 5 tháng 5 âm lịch, giờ Ngọ, Nam.

LaKinh hiển thị Thiên Cơ tại cung Mệnh.
Bốn website khác hiển thị Phá Quân.

Đây không phải sự khác biệt nhỏ. Thiên Cơ và Phá Quân là hai ngôi sao hoàn toàn khác nhau với ý nghĩa cuộc đời hoàn toàn khác nhau. Nếu ứng dụng nói ai đó là người Thiên Cơ trong khi họ thực ra là Phá Quân — đó không phải lỗi nhỏ. Đó là ứng dụng đang nói dối về căn số của người ta.


### Thuật Toán An Tử Vi

Công thức gốc của LaKinh:

const thuong = Math.floor(lunarDay / cucValue);
const du = lunarDay % cucValue;
if (du === 0) {
  index = thuong % 12;
} else {
  index = (thuong + cucValue - du) % 12;
}

Đơn giản. Gọn. Sai.

Thuật toán đúng — tìm được trong source code của thư viện iztro — dùng vòng lặp: tăng dần offset cho đến khi tìm được số chia hết. Sau đó dùng tính chẵn lẻ của offset để điều chỉnh vị trí cuối cùng.

Formula của tôi thiếu hoàn toàn vòng lặp offset này. Kết quả: đúng khi ngày chia hết cho Cục, sai với mọi trường hợp còn lại. Mà đa số các ngày không chia hết, nên đa số lá số đều sai.

Sau khi fix, kiểm tra 3 test case — tất cả đều pass.


### Bug Tứ Hóa: Canh Khoa Bị Mất

Ở test case thứ 4 (năm 1990), 14/14 chính tinh đúng. Nhưng tôi nhận ra bảng Tứ Hóa có lỗi: năm Canh, khoa bị gán cho Thái Dương — trùng với loc. Kết quả: Hóa Khoa biến mất trên lá số.

Cả tuvivietnam.vn lẫn kabala.vn đều hiển thị Hóa Khoa tại Thái Âm cho năm Canh. Sửa một dòng là xong.


### Bug Đổi Lịch: Ngày Sinh Trước 1975 Bị Lệch

Đây là bug bất ngờ nhất.

Tôi nhập ngày 15/03/1975 vào LaKinh. Ứng dụng hiển thị 14/03/1975.

Nguyên nhân: Việt Nam dùng UTC+8 trước tháng 6/1975. iOS biết dữ liệu lịch sử timezone này. Khi DateTimePicker trả về Date object cho năm 1975, nó dùng offset UTC+8. Khi string ngày được parse lại bằng new Date("1975-03-15") — JavaScript interpret là UTC midnight — ngày bị lùi một ngày.

Sâu hơn nữa, hàm đổi lịch Dương sang Âm cũng dùng local Date object để tính offset ngày. Hai Date object với timezone lịch sử khác nhau → sai lệch khi trừ nhau → lá số ngày sinh 1975 ra nhầm ngày âm lịch hoàn toàn (lệch ~19 ngày).

Fix: dùng Date.UTC() để tính thuần túy, không qua timezone.

Sau khi fix: 15/03/1975 ra đúng ngày 3 tháng 2 năm Ất Mão — khớp hoàn toàn với tuvivietnam.vn và kabala.vn.


### Vấn Đề Với tuviglobal.com

Trong quá trình test, tôi cũng kiểm tra tuviglobal.com — và nó liên tục cho kết quả khác với tuvivietnam.vn và kabala.vn.

Sau một hồi tìm hiểu, tôi nhận ra tuviglobal có vẻ theo chuẩn Tử Vi Trung Quốc — chia sẻ nguồn gốc lịch sử với Tử Vi Việt Nam nhưng đã phân kỳ đáng kể ở nhiều công thức. Truyền thống Việt Nam (như được thực hành bởi tuvivietnam.vn và kabala.vn) có chuẩn riêng của mình.

Quyết định của tôi: khi nghi ngờ, theo tuvivietnam.vn và kabala.vn. Đây là hai tham chiếu Tử Vi Việt Nam được dùng rộng rãi nhất. Khi cả hai đồng ý, đó là "ground truth" gần nhất tôi có thể có.


### Điều Không Ai Nói Với Bạn Về Domain Apps

Xây máy tính thì đơn giản: 2 + 2 = 4, không tranh cãi.

Xây ứng dụng domain với 2.000 năm truyền thống, nhiều trường phái cạnh tranh, và không có spec chính thức? Mỗi công thức đều cần kiểm tra chéo với tham chiếu thực tế. Và các tham chiếu đó đôi khi mâu thuẫn nhau.

Chậm. Mệt mỏi. Nhưng đây là cách duy nhất để xây thứ gì đó người dùng Việt Nam thực sự tin tưởng.


LaKinh là ứng dụng Tử Vi Việt Nam tôi đang xây một mình. Theo dõi hành trình: