While working on a utility site recently, I needed a precise age calculator. Sounds trivial, right? Pick two dates, subtract, done. Except it's not. The moment you try to calculate "25 years, 3 months, and 12 days" accurately, you realize that naive date arithmetic is a rabbit hole of edge cases.
Let me walk you through the engineering decisions that turned a seemingly simple tool into a proper date-handling exercise.
The Problem with Simple Subtraction
Here's the trap I initially fell into:
const ageInMilliseconds = Date.now() - birthDate.getTime();
const ageInYears = ageInMilliseconds / (365.25 * 24 * 60 * 60 * 1000);
This works... until it doesn't. The 365.25 days approximation drifts. Leap years aren't uniform. And try explaining to a user born on February 29th why their age is "wrong" on non-leap years.
The real issue is that calendars aren't arithmetic — they're cultural constructs with rules. Months have different lengths. Leap years follow a specific pattern. You can't just divide milliseconds by a constant and get a meaningful answer.
The "Correct" Way: Component-wise Calculation
The key insight is to calculate years, months, and days separately, adjusting for borrows like you learned in elementary school subtraction:
function calculateAge(birthDate, targetDate) {
let years = targetDate.getFullYear() - birthDate.getFullYear();
let months = targetDate.getMonth() - birthDate.getMonth();
let days = targetDate.getDate() - birthDate.getDate();
if (days < 0) {
months--;
days += daysInMonth(targetDate.getFullYear(), targetDate.getMonth() - 1);
}
if (months < 0) {
years--;
months += 12;
}
return { years, months, days };
}
This is the part that matters — the borrow logic. When your birthday hasn't happened yet this month, you borrow days from the previous month. When it's not your birthday month yet, you borrow months from the previous year. Simple, elegant, and handles all the calendar weirdness correctly.
The Birthday Problem
The next challenge was calculating days until the next birthday. The naive approach:
const nextBirthday = new Date(today.getFullYear(), birthDate.getMonth(), birthDate.getDate());
This breaks for people born on February 29th. What's their birthday in non-leap years? March 1st? February 28th?
The pragmatic answer: check if the birthday exists in the current year; if not, use March 1st. It's not perfect, but it's predictable and defensible:
function getBirthdayInYear(year, birthMonth, birthDay) {
const date = new Date(year, birthMonth, birthDay);
if (date.getMonth() !== birthMonth) {
return new Date(year, 2, 1); // March 1st
}
return date;
}
The Real-Time Seconds Problem
Here's where things get interesting. I wanted to show total seconds alive, and it needed to tick in real-time. The naive approach would recalculate everything every second. That's wasteful and causes jank.
The solution: calculate the static values once, then only update the seconds display:
let totalSeconds = initialSeconds;
setInterval(() => {
totalSeconds++;
document.getElementById('totalSeconds').textContent = formatNumber(totalSeconds);
}, 1000);
This is a micro-optimization, but it's the kind of thinking that prevents your tool from feeling sluggish. The rest of the calculation happens once; only the seconds counter updates.
The AI Collaboration Experience
Now for the part that made this project actually fun — building it with AI assistance. I've been experimenting with using AI as a pair programmer for utility tools, and this was a perfect test case.
The First Attempt
I gave the AI my requirements: "Create a single-file HTML age calculator with precise year/month/day calculation, total days/hours/minutes, and next birthday countdown."
The AI's first attempt was... surprisingly good. It nailed the structure, the styling, and the basic logic. But it had one critical bug: it used the millisecond division approach for age calculation. The output looked right for most dates, but it was subtly wrong for edge cases.
The Iteration Process
This is where the real collaboration began. Instead of pointing out the bug directly, I asked:
"What happens for someone born on February 29th, 2000, calculating age on March 1st, 2024?"
The AI paused, recalculated, and immediately recognized the issue. It suggested the component-wise approach. But its first implementation had a bug in the borrow logic — it didn't handle the case where the target date's month has fewer days than the birth month's day.
I had to guide it through the borrow logic step by step. The AI was great at generating code quickly, but it struggled with the edge case reasoning that comes naturally to experienced developers. It would optimize for the common case and miss the exceptions.
What Worked Well
The AI excelled at:
- Generating the complete HTML structure with proper i18n support
- Creating responsive CSS with dark mode support
- Writing the structured data for SEO
- Implementing the real-time seconds counter
Where I Had to Step In
The AI struggled with:
- Calendar edge cases (February 29th, month-end boundaries)
- The borrow logic in date arithmetic
- Understanding that not all dates are valid in all years
The i18n Trap
One thing the AI got right from the start: proper internationalization. It automatically used a key-value system with data-i18n attributes and a JavaScript dictionary. This was smart because it anticipated the need for multiple languages without over-engineering.
The language detection logic is simple but effective:
const lang = new URLSearchParams(window.location.search).get('lang')
|| navigator.language.startsWith('en') ? 'en' : 'zh';
URL parameter takes priority, then browser language, then default to Chinese. This pattern is simple enough to be maintainable but powerful enough for edge cases.
Lessons Learned
1. Calendar math is deceptively complex. What looks like a simple subtraction problem has more edge cases than you'd expect. Always test with boundary dates.
2. AI is a great pair programmer, but not a replacement for understanding. The AI generated code quickly, but I had to understand the domain deeply to guide it correctly. It's like having a brilliant intern who's fast but needs supervision.
3. Real-time features need incremental updates. Don't recalculate everything when you only need to update one value. Think about what actually needs to change.
4. Internationalization should be baked in from the start. Retrofitting i18n is painful. Designing for it from day one is barely any extra work.
The Result
I ended up with a clean, single-file tool that handles all the date edge cases correctly. It's fast, responsive, and works in both Chinese and English. The whole thing is about 300 lines of HTML/CSS/JS — no frameworks, no dependencies.
During this process, I built a small browser-based tool to make this workflow easier. You can find it at craftvo.app if you want to see the final result.
The real takeaway isn't about age calculators specifically. It's about the mindset: don't trust naive arithmetic when dealing with human constructs like calendars. And when using AI assistance, remember that it's a tool for accelerating your thinking, not replacing it. The best results come from understanding the problem deeply and using AI to execute your understanding faster.
Top comments (0)