DEV Community

Jan Wangrat
Jan Wangrat

Posted on

The bug that made my best-dates feature return nothing at 31 people

I built a small thing for picking the dates of a group trip: everyone taps the days they are free, and it ranks the stretches of days that work for the most people. No accounts, just a link you share.

The first version of that ranking enumerated every subset of participants:

for (let mask = 1; mask < (1 << n); mask++) {
  // find the days everyone in this subset is free
}
Enter fullscreen mode Exit fullscreen mode

Two problems. The second one is the interesting one.

2^n iterations locks up the tab somewhere around twenty people. Annoying, expected, fixable later.

At n = 31, 1 << 31 is negative. Bitwise operators in JS coerce to int32, the sign bit flips, and you get -2147483648. So mask < (1 << 31) is false on the very first check, the loop body never runs, and the function returns an empty list. No error, no warning. The feature quietly stops existing.

Worse than that: past 31 participants it didn't fail, it answered wrong. On a 35-person test trip where all 35 people were free for the same five days, it offered that range to 6 of them.

The fix wasn't a bigger loop. The subsets were never the interesting objects. For any given stretch of days, the people who can make all of it are already determined: they are the intersection of who is free on each day. So walk the ranges and carry that intersection.

// availability[day] is a BigInt bitmask of who is free that day
let members = availability[start];
for (let day = start + 1; day <= end; day++) {
  members &= availability[day];
}
Enter fullscreen mode Exit fullscreen mode

Emit a candidate wherever the running intersection is about to shrink, and every set you emit is maximal for its range by construction. That also deleted a separate "same dates but more people" dominance check I no longer needed.

BigInt instead of a 32-bit number is the part that actually matters. Number would have moved the cliff to 53 and left the same class of bug sitting there. Sixty people across ninety days now answers in under three seconds.

Two things I took away.

Silent empty results are worse than exceptions. The 2^n slowness showed up the first time I tested with a realistic group. The n = 31 cliff needed someone to have 31 friends.

And: bitwise operators in JavaScript are int32. I knew that. I still wrote 1 << n with n coming from user data.

The rest of the stack is deliberately boring. React on Cloudflare Pages, Pages Functions for the API, D1 for storage, no auth provider, no accounts at all - identity is a name you type plus possession of the link.

It's at wegowhen.com if you want to poke at it, and the source is on GitHub.

Fix and write-up done with Claude Code.

Top comments (1)

Collapse
 
david_william_4807bc10ccc profile image
David William

Really interesting write-up. The 1 << n issue is a great example of how a seemingly harmless JavaScript bitwise operation can create a completely silent failure once real-world input reaches the int32 boundary.

I also like the shift from enumerating participant subsets to maintaining the running intersection of availability. It’s a much more natural representation of the problem and removes the need for the separate dominance check.

Nice work identifying not just the symptom, but the underlying algorithmic issue. The BigInt change makes the solution much more robust for larger groups as well.