DEV Community

ggwork
ggwork

Posted on

How I Built a Birthday Number Generator That Actually Makes Sense (Sort Of)

Last week, I found myself staring at my calendar, wondering if there was any meaningful way to derive lottery numbers from a birthday. Not because I believe in lucky numbers — I'm a developer, I believe in deterministic algorithms — but because I kept seeing friends post their "birthday numbers" on social media, and I wanted to understand the logic (or lack thereof) behind it.

So I did what any reasonable developer would do: I built a tool for it.

The Problem: Random Isn't Random Enough

Here's the thing about lottery number generators — most of them just use Math.random(). That's fine if you want truly random numbers, but it's boring. Nobody feels a connection to a number that came from a JavaScript PRNG seed.

What people actually want is a story behind their numbers. "My birthday gives me these numbers" sounds way better than "a random number generator gave me these numbers." It's the difference between having a meaningful ritual and just gambling.

The challenge: how do you map a date like 1990-05-20 to a valid set of lottery numbers in a way that feels intentional, produces valid ranges, and doesn't require a server?

The "Algorithm" (I Use That Term Loosely)

Let me walk you through the logic, because it's actually a fun little puzzle.

For a Double Color Ball (双色球), you need 6 red balls (1-33) and 1 blue ball (1-16). For 3D/P3, you need 3 digits (0-9).

The naive approach would be something like:

// Don't do this
const redBall = year % 33;
Enter fullscreen mode Exit fullscreen mode

This fails immediately because:

  • 1990 % 33 = 10 — fine, but what about the month and day?
  • You need 6 unique numbers, and simple modulo arithmetic will produce duplicates fast
  • map33 needs to handle negative numbers and zero gracefully

The actual solution came from a classic programming trick I remembered from implementing hash functions:

const map33 = (n) => ((n - 1) % 33 + 33) % 33 + 1;
Enter fullscreen mode Exit fullscreen mode

This is the "true modulo" pattern — it handles negative numbers correctly, which matters more than you'd think. When you're deriving numbers from date components and custom lucky numbers, you can easily end up with negative intermediate values.

The AI Collaboration: Where It Helped and Where It Didn't

Full disclosure: I built most of this with an AI assistant. Here's what that actually looked like.

What AI Got Right

I started with a prompt like: "Build me a tool that takes a birthday and generates lottery numbers. It should map date components to valid ranges and support custom lucky numbers."

The AI immediately understood the domain. It knew about 双色球's structure (6+1), it knew about 3D's 3-digit format, and it generated the map33 function correctly on the first try. That's genuinely impressive — it's not just pattern-matching; it's understanding the constraints of the problem.

Where It Stumbled

The first version had a critical bug: it didn't handle duplicate numbers. If your birthday somehow produced the same number twice (which happens more often than you'd think), you'd end up with 5 red balls instead of 6. The AI's solution was to just... not handle it. Classic.

I had to prompt it specifically: "What happens if the date components map to the same number? How do you ensure 6 unique red balls?" That's when it added the deduplication logic with a "fill from the edges" approach — if you can't get enough unique numbers from the date, it pulls from the extremes (1 and 33) to fill the gaps.

The CSS Nightmare

Here's where AI really struggled: the styling. The tool needed to work in both light and dark mode, look decent on mobile, and present the numbers in a way that felt like a lottery ticket.

The AI's first CSS attempt was a mess. It used fixed pixel values everywhere, didn't handle the prefers-color-scheme media query properly, and the ticket layout broke on mobile. I had to iterate on the CSS more than the JavaScript — which, if you've done any web development, you know is the real truth of frontend work.

The final version uses CSS custom properties for theming:

:root {
  --bg: #ffffff;
  --text: #111827;
  --primary: #3b82f6;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1a1a2e;
    --text: #e2e8f0;
    --primary: #60a5fa;
  }
}
Enter fullscreen mode Exit fullscreen mode

This made the dark mode transition trivial — no JavaScript, no class toggling, just good CSS architecture.

The Architecture Decision: Zero Dependencies

I made a deliberate choice early on: this tool would have zero dependencies. No React, no Vue, no build step. Just vanilla HTML, CSS, and JavaScript in a single file.

Why? Because the tool is so simple that any framework would be overkill. The entire logic is about 50 lines of JavaScript. Adding React to this would be like using a flamethrower to light a candle.

But here's the trade-off I had to accept: no framework means I'm responsible for everything. State management? That's just a variable. DOM updates? That's innerHTML. Internationalization? That's a dictionary object and a language detector.

The i18n was actually a fun challenge. The tool needs to support both Chinese and English, and I didn't want to load a library for that. The solution was a simple dictionary pattern:

const I18N = {
  zh: { title: "生日幸运数字选号器", gen: "生成号码" },
  en: { title: "Birthday Number Picker", gen: "Generate" }
};

function detectLang() {
  const params = new URLSearchParams(location.search);
  if (params.get('lang') === 'en') return 'en';
  return navigator.language.startsWith('zh') ? 'zh' : 'en';
}
Enter fullscreen mode Exit fullscreen mode

Language detection via URL parameter first, then falls back to browser settings. It's not perfect — someone with a Chinese browser but English preference gets the short end — but it covers 95% of cases without any external services.

The "Why" Behind Every Decision

Why client-side only?

Privacy. If someone enters their birthday, that's personal data. Sending it to a server feels unnecessary and potentially creepy. With everything client-side, the data never leaves the browser. It's also faster — no network requests, instant response.

The trade-off: I can't track usage patterns or improve the "algorithm" based on user behavior. But for a fun utility tool, that's a good trade.

Why the specific number mapping?

I could have done something more complex — like hashing the date string and using the hash as a seed. But that feels too opaque. The whole point is that the user can see the connection between their birthday and the numbers. So I kept it transparent:

  • Year's last two digits → maps to a red ball
  • Month → maps to a red ball
  • Day → maps to a red ball
  • Custom lucky numbers → each maps to a red ball
  • Blue ball comes from a combination of all components

This way, when someone asks "where did this number come from?", you can actually explain it.

Why the disclaimer?

Because I have to be responsible. This tool is for entertainment, and it's easy for people to get superstitious about numbers. The disclaimer is front and center:

本工具按趣味规则将日期转换为号码,与开奖毫无关联,中奖概率不因此而改变。请理性购彩,量力而行。

It's not legal advice — I'm not a lawyer — but it's honest. The tool doesn't change your odds. It just makes the numbers feel meaningful.

Performance: Because Even Simple Things Can Be Slow

The whole tool runs in microseconds, but I still thought about performance:

  • No framework runtime: The page loads instantly because there's nothing to parse
  • No external assets: No CDN fonts, no icon libraries, no analytics scripts
  • Efficient DOM updates: Only update the result container when generating new numbers

The entire page is under 4KB of HTML, CSS, and JavaScript. That's smaller than a single image file. It loads faster than you can blink.

What I Learned

1. AI is great at constraints, bad at edge cases

The AI understood the problem domain well but kept missing edge cases: duplicates, empty input, invalid dates. I had to explicitly ask about these. It's like pair programming with a brilliant but inexperienced developer — you have to think about what they're not thinking about.

2. CSS is still the hardest part

Every time. The JavaScript logic was solved in minutes. The CSS took hours. And the most important part wasn't the layout — it was the theming system that respects user preferences.

3. Simplicity is a feature

I could have made this a full app with accounts, saved numbers, and sharing features. But the constraint — single file, no dependencies, instant load — forced better decisions. The tool does one thing and does it well.

4. Entertainment tools need to be honest

There's a fine line between fun and misleading. The tool is explicitly labeled as entertainment, and the disclaimer is prominent. You can have fun with numbers without pretending there's magic involved.

The Result

The tool works. You pick a date, optionally add lucky numbers, and get a set of numbers that feel connected to your birthday. It's not going to change your odds of winning — nothing will — but it makes the ritual more meaningful.

If you're curious, you can try it here: Birthday Number Picker

The entire thing is a single HTML file. No build step, no dependencies, no server. Just a date input, a few math functions, and a CSS custom property for dark mode.

And honestly? That's exactly what a tool like this should be.

Top comments (0)