@pipeworx/mcp-edge-case-museum 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +17 -12
  2. package/bin/cli.js +55 -11
  3. package/lib/index.js +333 -0
  4. package/package.json +17 -16
  5. package/server.json +1 -1
  6. package/tools.json +98 -0
  7. package/src/bake.mjs +0 -112
  8. package/src/ecm-index-data.ts +0 -15
  9. package/src/exhibits/datetime/add-24-hours-vs-add-1-day-across-dst.json +0 -51
  10. package/src/exhibits/datetime/add-365-days-to-feb-29-not-next-feb-29.json +0 -49
  11. package/src/exhibits/datetime/age-calculator-feb29-birthday-crash-vs-silent.json +0 -52
  12. package/src/exhibits/datetime/century-leap-year-1900-not-leap.json +0 -68
  13. package/src/exhibits/datetime/datetime-utcnow-naive-deprecated.json +0 -39
  14. package/src/exhibits/datetime/dst-fall-back-ambiguous-fold.json +0 -51
  15. package/src/exhibits/datetime/dst-spring-forward-nonexistent-time.json +0 -57
  16. package/src/exhibits/datetime/fractional-utc-offsets-kathmandu-kolkata.json +0 -50
  17. package/src/exhibits/datetime/iso-week-year-vs-calendar-year.json +0 -48
  18. package/src/exhibits/datetime/js-date-field-overflow-rollover.json +0 -49
  19. package/src/exhibits/datetime/leap-second-rejected-vs-silently-invalid.json +0 -57
  20. package/src/exhibits/datetime/lord-howe-30-minute-dst.json +0 -47
  21. package/src/exhibits/datetime/midnight-that-doesnt-exist-sao-paulo.json +0 -53
  22. package/src/exhibits/datetime/python-naive-aware-comparison-typeerror.json +0 -46
  23. package/src/exhibits/datetime/samoa-skipped-day-2011.json +0 -50
  24. package/src/exhibits/datetime/southern-hemisphere-dst-reversed.json +0 -50
  25. package/src/exhibits/datetime/two-digit-year-century-pivot-mismatch.json +0 -49
  26. package/src/exhibits/datetime/year-2038-int32-overflow.json +0 -52
  27. package/src/exhibits/filenames/absolute-second-argument-resets-join.json +0 -62
  28. package/src/exhibits/filenames/bidi-override-extension-spoof.json +0 -58
  29. package/src/exhibits/filenames/case-insensitive-filesystem-pure-path-mismatch.json +0 -74
  30. package/src/exhibits/filenames/dotfile-has-no-extension.json +0 -61
  31. package/src/exhibits/filenames/extension-trap-double-extension.json +0 -62
  32. package/src/exhibits/filenames/filename-length-limits-bytes-not-chars.json +0 -66
  33. package/src/exhibits/filenames/leading-dash-filename-parsed-as-flag.json +0 -67
  34. package/src/exhibits/filenames/nfc-nfd-filename-lookup-mismatch.json +0 -68
  35. package/src/exhibits/filenames/ntfs-alternate-data-stream-colon.json +0 -59
  36. package/src/exhibits/filenames/ntpath-splitdrive-unc-paths.json +0 -58
  37. package/src/exhibits/filenames/nul-byte-in-filename-raises-at-open.json +0 -60
  38. package/src/exhibits/filenames/path-join-escapes-the-base-directory.json +0 -63
  39. package/src/exhibits/filenames/windows-backslash-traversal-ntpath.json +0 -62
  40. package/src/exhibits/filenames/windows-reserved-device-names.json +0 -73
  41. package/src/exhibits/filenames/windows-trailing-dot-space-stripped.json +0 -77
  42. package/src/exhibits/float-money/decimal-from-float-vs-string.json +0 -66
  43. package/src/exhibits/float-money/float-anchor-0-1-plus-0-2.json +0 -67
  44. package/src/exhibits/float-money/intl-number-format-de-de-roundtrip-misparsed.json +0 -62
  45. package/src/exhibits/float-money/iso4217-minor-units-not-always-two.json +0 -78
  46. package/src/exhibits/float-money/math-abs-negative-zero.json +0 -51
  47. package/src/exhibits/float-money/max-safe-integer-plus-two.json +0 -62
  48. package/src/exhibits/float-money/naive-loop-sum-order-dependence.json +0 -71
  49. package/src/exhibits/float-money/nan-breaks-equality-and-max-min.json +0 -70
  50. package/src/exhibits/float-money/negative-zero-balance-formatting.json +0 -74
  51. package/src/exhibits/float-money/number-epsilon-false-positive-small-magnitude.json +0 -52
  52. package/src/exhibits/float-money/parsefloat-comma-thousands-separator.json +0 -66
  53. package/src/exhibits/float-money/python-banker-rounding-vs-js-round-half-up.json +0 -65
  54. package/src/exhibits/float-money/python-round-2-675-float-imprecision.json +0 -64
  55. package/src/exhibits/float-money/splitting-100-dollars-three-ways-leaves-a-cent.json +0 -63
  56. package/src/exhibits/float-money/sum-ten-dimes-not-a-dollar.json +0 -77
  57. package/src/exhibits/float-money/tofixed-1-005-rounds-down-not-up.json +0 -63
  58. package/src/exhibits/float-money/tofixed-2-345-inconsistent-rounding.json +0 -64
  59. package/src/exhibits/integers/array-index-above-2-32-2-not-an-index.json +0 -42
  60. package/src/exhibits/integers/bigint-number-mixing-typeerror.json +0 -49
  61. package/src/exhibits/integers/bitwise-32-bit-truncation.json +0 -56
  62. package/src/exhibits/integers/bool-is-int-subclass-dict-key-collision.json +0 -58
  63. package/src/exhibits/integers/floor-div-mod-sign-python-vs-js.json +0 -48
  64. package/src/exhibits/integers/json-parse-loses-id-precision.json +0 -58
  65. package/src/exhibits/integers/max-safe-integer-plus-one-equals-plus-two.json +0 -58
  66. package/src/exhibits/integers/number-empty-and-whitespace-string-equals-zero.json +0 -55
  67. package/src/exhibits/integers/parseint-null-radix36-digit-soup.json +0 -57
  68. package/src/exhibits/integers/parseint-number-argument-scientific-notation.json +0 -48
  69. package/src/exhibits/integers/parseint-stops-at-first-invalid-char-vs-number.json +0 -58
  70. package/src/exhibits/integers/python-int-accepts-fullwidth-unicode-digits.json +0 -56
  71. package/src/exhibits/integers/python-int-accepts-underscores-and-whitespace.json +0 -48
  72. package/src/exhibits/integers/python-unbounded-int-vs-fixed-width-wrap.json +0 -49
  73. package/src/exhibits/names-addresses/address-with-no-street-number.json +0 -54
  74. package/src/exhibits/names-addresses/ascii-regex-rejects-accented-and-hyphenated-names.json +0 -55
  75. package/src/exhibits/names-addresses/house-number-suffix-collapsed-by-digit-extraction.json +0 -55
  76. package/src/exhibits/names-addresses/mononym-fails-first-last-required.json +0 -56
  77. package/src/exhibits/names-addresses/naive-lowercase-breaks-greek-final-sigma.json +0 -64
  78. package/src/exhibits/names-addresses/null-surname-coerced-to-null-value.json +0 -58
  79. package/src/exhibits/names-addresses/single-letter-surname-fails-min-length.json +0 -55
  80. package/src/exhibits/names-addresses/sort-by-surname-particle-convention-mismatch.json +0 -57
  81. package/src/exhibits/names-addresses/spanish-double-surname-treated-as-single.json +0 -55
  82. package/src/exhibits/names-addresses/split-surname-on-last-space-loses-particle.json +0 -59
  83. package/src/exhibits/names-addresses/title-case-mangles-mixed-case-and-apostrophes.json +0 -60
  84. package/src/exhibits/names-addresses/uk-canada-postal-code-assumed-numeric.json +0 -55
  85. package/src/exhibits/names-addresses/us-zip-code-leading-zero-lost-as-integer.json +0 -58
  86. package/src/exhibits/names-addresses/varchar-truncation-splits-name-mid-grapheme.json +0 -63
  87. package/src/exhibits/parsing/csv-crlf-trailing-blank-line.json +0 -87
  88. package/src/exhibits/parsing/csv-doubled-quote-escaping.json +0 -70
  89. package/src/exhibits/parsing/csv-formula-injection-excel.json +0 -71
  90. package/src/exhibits/parsing/csv-quoted-field-with-comma.json +0 -70
  91. package/src/exhibits/parsing/csv-quoted-field-with-newline.json +0 -70
  92. package/src/exhibits/parsing/csv-semicolon-delimited-european-excel.json +0 -70
  93. package/src/exhibits/parsing/csv-utf8-bom-first-header.json +0 -74
  94. package/src/exhibits/parsing/email-regex-rejects-valid-apostrophe-address.json +0 -68
  95. package/src/exhibits/parsing/json-big-integer-precision-loss.json +0 -65
  96. package/src/exhibits/parsing/json-duplicate-keys-last-wins.json +0 -64
  97. package/src/exhibits/parsing/json-lone-surrogate-roundtrip.json +0 -58
  98. package/src/exhibits/parsing/json-nan-infinity-not-valid-json.json +0 -54
  99. package/src/exhibits/parsing/url-backslash-treated-as-slash-whatwg.json +0 -60
  100. package/src/exhibits/parsing/url-urlparse-never-raises-on-garbage.json +0 -60
  101. package/src/exhibits/parsing/url-userinfo-trusted-looking-host.json +0 -60
  102. package/src/exhibits/sorting/accented-letters-sort-after-z.json +0 -63
  103. package/src/exhibits/sorting/byte-order-caps-before-lowercase.json +0 -66
  104. package/src/exhibits/sorting/case-insensitive-sort-lower-vs-casefold.json +0 -56
  105. package/src/exhibits/sorting/comparator-returns-boolean-js.json +0 -49
  106. package/src/exhibits/sorting/js-array-sort-stringifies-numbers.json +0 -68
  107. package/src/exhibits/sorting/locale-collation-de-vs-sv.json +0 -54
  108. package/src/exhibits/sorting/localecompare-numeric-option.json +0 -50
  109. package/src/exhibits/sorting/nan-in-sort.json +0 -59
  110. package/src/exhibits/sorting/natural-sort-breaks-lexicographic.json +0 -63
  111. package/src/exhibits/sorting/postgres-collation-documented.json +0 -61
  112. package/src/exhibits/sorting/python-cannot-compare-str-int.json +0 -70
  113. package/src/exhibits/sorting/reverse-sort-vs-reverse-after-sort-ties.json +0 -48
  114. package/src/exhibits/sorting/sorting-mixed-none-null.json +0 -56
  115. package/src/exhibits/sorting/typed-array-sort-is-numeric-by-default.json +0 -48
  116. package/src/exhibits/unicode/arabic-indic-digit-parses.json +0 -73
  117. package/src/exhibits/unicode/bidi-override-filename-spoof.json +0 -62
  118. package/src/exhibits/unicode/cyrillic-homoglyph-paypal-spoof.json +0 -67
  119. package/src/exhibits/unicode/fullwidth-bypasses-keyword-filter.json +0 -61
  120. package/src/exhibits/unicode/german-eszett-case-fold.json +0 -72
  121. package/src/exhibits/unicode/grapheme-vs-codepoint-length.json +0 -57
  122. package/src/exhibits/unicode/greek-final-sigma-casefold.json +0 -54
  123. package/src/exhibits/unicode/js-word-regex-ascii-only.json +0 -64
  124. package/src/exhibits/unicode/nfc-nfd-same-name-two-accounts.json +0 -70
  125. package/src/exhibits/unicode/nfkc-compatibility-folding.json +0 -63
  126. package/src/exhibits/unicode/truncation-orphans-combining-mark.json +0 -62
  127. package/src/exhibits/unicode/truncation-splits-surrogate-pair.json +0 -65
  128. package/src/exhibits/unicode/turkish-dotless-i-case-mapping.json +0 -72
  129. package/src/exhibits/unicode/zalgo-combining-mark-stacking.json +0 -57
  130. package/src/exhibits/unicode/zero-width-space-username.json +0 -62
  131. package/src/harness.mjs +0 -331
  132. package/src/index.ts +0 -686
  133. package/src/server.ts +0 -49
  134. package/src/verified.json +0 -977
  135. package/tsconfig.json +0 -18
@@ -1,51 +0,0 @@
1
- {
2
- "id": "add-24-hours-vs-add-1-day-across-dst",
3
- "title": "In Python, 'add 24 hours' and 'add 1 day' land on the same wall-clock minute across a DST jump",
4
- "category": "datetime",
5
- "assumption": "Adding a timedelta of 24 hours to a timezone-aware datetime moves it forward by exactly 24 real hours, the same way adding 1 day does if you just mean 'tomorrow'.",
6
- "input": {
7
- "display": "datetime(2024, 3, 9, 12, 0, tzinfo=ZoneInfo(\"America/New_York\")) + timedelta(hours=24) vs + timedelta(days=1)",
8
- "description": "A 'remind me in 24 hours' feature, or a billing-cycle / subscription renewal computed by adding a fixed duration to a timestamp, run across the March DST change."
9
- },
10
- "what_goes_wrong": "A 'remind me in exactly 24 hours' feature built on Python's aware-datetime arithmetic fires at the same wall-clock minute as 'remind me tomorrow', not 24 real hours later -- on the spring-forward day the two features that were supposed to differ by design turn out identical, and neither one is actually 24 elapsed hours.",
11
- "why": "CPython's datetime arithmetic for aware datetimes adds the timedelta to the naive (wall-clock) fields first, then reinterprets the result in the same zone -- it does not convert to UTC, add, and convert back. So timedelta(hours=24) and timedelta(days=1) both just add to the naive calendar fields and land on the same wall-clock time, 2024-03-10 12:00, even though only 23 real hours separate the two instants because of the lost hour. Subtraction follows the same rule: subtracting two datetimes that share a tzinfo returns the wall-clock difference (1 day here, not the 23 real hours); Python only goes through UTC when the operands have different tzinfo objects. JavaScript's Date stores a UTC millisecond count, so adding 24*3600*1000 to getTime() is 24 real elapsed hours and renders as 13:00, not 12:00, in the DST-affected zone (the repro). JavaScript also has wall-clock arithmetic, but only in the host's local zone and only through the local setters: in a process running in America/New_York, both setDate(getDate() + 1) and setHours(getHours() + 24) land on 12:00 the next day, 23 real hours later, just like Python.",
12
- "affects": [
13
- "python",
14
- "javascript (Date local-time setters such as setHours/setDate)"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "from zoneinfo import ZoneInfo\nfrom datetime import datetime, timedelta\n\ntz = ZoneInfo(\"America/New_York\")\nstart = datetime(2024, 3, 9, 12, 0, tzinfo=tz)\nplus_24h = start + timedelta(hours=24)\nplus_1day = start + timedelta(days=1)\nprint(\"start:\", start, \"| +24h:\", plus_24h, \"| +1day:\", plus_1day)\nprint(\"+24h == +1day (same wall clock):\", plus_24h == plus_1day)\nprint(\"+24h in UTC:\", plus_24h.astimezone(ZoneInfo(\"UTC\")))\nprint(\"real elapsed hours:\", (plus_24h.astimezone(ZoneInfo(\"UTC\")) - start.astimezone(ZoneInfo(\"UTC\"))).total_seconds() / 3600)\n",
20
- "expected_output": "start: 2024-03-09 12:00:00-05:00 | +24h: 2024-03-10 12:00:00-04:00 | +1day: 2024-03-10 12:00:00-04:00\n+24h == +1day (same wall clock): True\n+24h in UTC: 2024-03-10 16:00:00+00:00\nreal elapsed hours: 23.0",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "// JS Date arithmetic is always real elapsed time (UTC milliseconds).\nconst startUtc = Date.UTC(2024, 2, 9, 17, 0, 0); // 2024-03-09 12:00 America/New_York\nconst plus24hUtc = startUtc + 24 * 3600 * 1000;\nconst fmt = new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York', hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });\nconsole.log('start local:', fmt.format(new Date(startUtc)));\nconsole.log('start + 24 real hours, local:', fmt.format(new Date(plus24hUtc)));\n",
26
- "expected_output": "start local: 03/09/2024, 12:00\nstart + 24 real hours, local: 03/10/2024, 13:00",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Decide explicitly whether you mean 'N real hours of elapsed time' (convert to UTC, add, convert back -- or just do the arithmetic in UTC) or 'the same wall-clock time N calendar days later' (use calendar-aware addition that is documented to do that, e.g. `dateutil.relativedelta`). Never assume Python's aware `timedelta` arithmetic gives you the first one; it gives you something closer to the second.",
31
- "related": [
32
- "dst-spring-forward-nonexistent-time"
33
- ],
34
- "sources": [
35
- {
36
- "title": "Python docs: datetime -- Supported operations",
37
- "url": "https://docs.python.org/3/library/datetime.html#datetime.datetime"
38
- },
39
- {
40
- "title": "IANA Time Zone Database",
41
- "url": "https://www.iana.org/time-zones"
42
- }
43
- ],
44
- "tags": [
45
- "timezone",
46
- "scheduling",
47
- "reminder",
48
- "timestamps"
49
- ],
50
- "test_cases": []
51
- }
@@ -1,49 +0,0 @@
1
- {
2
- "id": "add-365-days-to-feb-29-not-next-feb-29",
3
- "title": "A Feb-29 birthday plus 365 days is Feb 28 of the next year, not another Feb 29",
4
- "category": "datetime",
5
- "assumption": "Adding 365 days to a date gives you 'the same date, one year later'.",
6
- "input": {
7
- "display": "date(2000, 2, 29) + timedelta(days=365)",
8
- "description": "A 'renews annually' or '1 year from now' feature implemented as 'add 365 days' instead of 'add 1 to the year field'."
9
- },
10
- "what_goes_wrong": "A subscription or anniversary calculated as 'start date + timedelta(days=365)' for someone whose start date is February 29 lands on February 28 the following year, one day short of their actual next anniversary (which, in a non-leap year, does not exist on the calendar at all).",
11
- "why": "`timedelta(days=365)` counts elapsed days; it has no concept of 'the same date next year'. The 365 days after 2000-02-29 run from March 2000 through February 2001, a stretch with no leap day in it, so they end on 2001-02-28; 2001-03-01 is 366 days away. The same drift hits any date in a leap year that falls before Feb 29: 2024-01-15 + 365 days is 2025-01-14, not 2025-01-15.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "from datetime import date, timedelta\n\nstart = date(2000, 2, 29)\nplus_365 = start + timedelta(days=365)\nprint(\"start:\", start, \"| +365 days:\", plus_365)\n",
20
- "expected_output": "start: 2000-02-29 | +365 days: 2001-02-28",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "const start = new Date(Date.UTC(2000, 1, 29));\nconst plus365 = new Date(start.getTime() + 365 * 86400 * 1000);\nconsole.log('start:', start.toISOString().slice(0, 10), '| +365 days:', plus365.toISOString().slice(0, 10));\n",
26
- "expected_output": "start: 2000-02-29 | +365 days: 2001-02-28",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Decide explicitly whether 'in N years' means 'add N to the year field, clamping Feb 29 to Feb 28 (or rolling to Mar 1) if needed' or 'add exactly N*365.25 days'; use a calendar-aware date library's year-addition function (e.g. `dateutil.relativedelta`) rather than `timedelta(days=365)` for anything billed as 'annual'.",
31
- "related": [
32
- "century-leap-year-1900-not-leap",
33
- "age-calculator-feb29-birthday-crash-vs-silent"
34
- ],
35
- "sources": [
36
- {
37
- "title": "Python docs: datetime -- timedelta objects",
38
- "url": "https://docs.python.org/3/library/datetime.html#timedelta-objects"
39
- }
40
- ],
41
- "tags": [
42
- "date parsing",
43
- "scheduling",
44
- "age calculation",
45
- "subscription",
46
- "billing"
47
- ],
48
- "test_cases": []
49
- }
@@ -1,52 +0,0 @@
1
- {
2
- "id": "age-calculator-feb29-birthday-crash-vs-silent",
3
- "title": "Computing 'this year's birthday' for someone born on Feb 29 crashes in Python and silently becomes March 1 in JS",
4
- "category": "datetime",
5
- "assumption": "You can always construct 'this year's version' of someone's birthday by combining the current year with their birth month and day.",
6
- "input": {
7
- "display": "date(2023, 2, 29) (constructing the 2023 birthday of someone born 2000-02-29, where 2023 is not a leap year)",
8
- "description": "Anniversary/birthday reminder features, age-at-next-birthday calculations, and loyalty-program 'birthday month' logic for anyone born on February 29."
9
- },
10
- "what_goes_wrong": "A 'days until your next birthday' feature for a Feb-29 user either crashes outright (Python) or silently reports their birthday as March 1st (JavaScript) in every year that isn't a leap year -- and the two backends disagree with each other about which day that is.",
11
- "why": "February 29 only exists in leap years. Python's `date()` constructor validates the day against the given year and raises `ValueError` for `date(2023, 2, 29)` because 2023 is not a leap year. JavaScript's `Date` constructor performs no such validation: field overflow silently rolls forward, so `new Date(2023, 1, 29)` (month index 1 = February) normalizes to March 1, 2023. Neither answer is 'the real February 29 of 2023' because there isn't one; the two runtimes just fail in opposite styles, loud versus silent.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "from datetime import date\n\nbirth = date(2000, 2, 29)\nreference_year = 2023 # not a leap year\ntry:\n this_years_birthday = date(reference_year, birth.month, birth.day)\n print(\"birthday:\", this_years_birthday)\nexcept ValueError as e:\n print(\"error constructing this year's birthday:\", e)\n",
20
- "expected_output": "error constructing this year's birthday: day 29 must be in range 1..28 for month 2 in year 2023",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "const birthMonth = 1; // February, 0-indexed\nconst birthDay = 29;\nconst referenceYear = 2023; // not a leap year\nconst thisYearsBirthday = new Date(referenceYear, birthMonth, birthDay);\nconsole.log('constructed date:', thisYearsBirthday.getFullYear(), thisYearsBirthday.getMonth(), thisYearsBirthday.getDate());\n",
26
- "expected_output": "constructed date: 2023 2 1",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Decide and document a policy for Feb-29 anniversaries in non-leap years (commonly Feb 28 or Mar 1) and apply it explicitly -- check `calendar.isleap(year)` first and substitute the chosen fallback day, rather than letting the date constructor decide by crashing or overflowing.",
31
- "related": [
32
- "add-365-days-to-feb-29-not-next-feb-29",
33
- "century-leap-year-1900-not-leap"
34
- ],
35
- "sources": [
36
- {
37
- "title": "Python docs: datetime.date",
38
- "url": "https://docs.python.org/3/library/datetime.html#datetime.date"
39
- },
40
- {
41
- "title": "ECMA-262: Date constructor (field overflow)",
42
- "url": "https://tc39.es/ecma262/#sec-date-constructor"
43
- }
44
- ],
45
- "tags": [
46
- "age calculation",
47
- "date parsing",
48
- "validation",
49
- "reminder"
50
- ],
51
- "test_cases": []
52
- }
@@ -1,68 +0,0 @@
1
- {
2
- "id": "century-leap-year-1900-not-leap",
3
- "title": "1900 was not a leap year, even though it's divisible by 4",
4
- "category": "datetime",
5
- "assumption": "A year is a leap year exactly when it's divisible by 4.",
6
- "input": {
7
- "display": "year % 4 == 0 for 1900 (True) vs the Gregorian rule's actual answer (False)",
8
- "description": "A hand-rolled leap-year check (common in date-math utilities, age calculators, or anything that didn't reach for the standard library) applied to any century year."
9
- },
10
- "what_goes_wrong": "A hand-written `is_leap(y) = y % 4 == 0` check says 1900 and 2100 are leap years; both are not, and code that trusts the check will construct a nonexistent February 29 or miscount days in centuries not divisible by 400.",
11
- "why": "The Gregorian calendar's actual rule is: divisible by 4 AND (not divisible by 100 OR divisible by 400). Century years are leap only if divisible by 400 -- 1900 and 2100 fail that (divisible by 100, not by 400) and are NOT leap years, while 2000 passes (divisible by 400) and IS one. The simpler 'divisible by 4' rule most developers remember is only an approximation: it is right for 397 of every 400 years and silently wrong for exactly these three century years in each 400.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "import calendar\n\ndef naive_is_leap(y):\n return y % 4 == 0\n\nfor y in (1900, 2000, 2024, 2100):\n print(y, \"naive:\", naive_is_leap(y), \"actual:\", calendar.isleap(y))\n\ntry:\n from datetime import date\n date(1900, 2, 29)\nexcept ValueError as e:\n print(\"constructing Feb 29, 1900 raises:\", e)\n",
20
- "expected_output": "1900 naive: True actual: False\n2000 naive: True actual: True\n2024 naive: True actual: True\n2100 naive: True actual: False\nconstructing Feb 29, 1900 raises: day 29 must be in range 1..28 for month 2 in year 1900",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "function naiveIsLeap(y) { return y % 4 === 0; }\nfunction actualIsLeap(y) { return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0; }\nfor (const y of [1900, 2000, 2024, 2100]) {\n console.log(y, 'naive:', naiveIsLeap(y), 'actual:', actualIsLeap(y));\n}\n// new Date silently rolls Feb 29 of a non-leap century year into March 1\nconst d = new Date(1900, 1, 29);\nconsole.log('new Date(1900, 1, 29) ->', d.getFullYear(), d.getMonth(), d.getDate());\n",
26
- "expected_output": "1900 naive: true actual: false\n2000 naive: true actual: true\n2024 naive: true actual: true\n2100 naive: true actual: false\nnew Date(1900, 1, 29) -> 1900 2 1",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Use the standard library's leap-year check (Python `calendar.isleap`, or construct the date and catch the error) instead of a hand-rolled `% 4` test. If you must write it yourself, the full rule is `(y % 4 == 0 and y % 100 != 0) or y % 400 == 0`.",
31
- "related": [
32
- "add-365-days-to-feb-29-not-next-feb-29"
33
- ],
34
- "sources": [
35
- {
36
- "title": "Python docs: calendar.isleap",
37
- "url": "https://docs.python.org/3/library/calendar.html#calendar.isleap"
38
- },
39
- {
40
- "title": "ISO 8601 / Gregorian calendar leap year rule (Wikipedia, background)",
41
- "url": "https://en.wikipedia.org/wiki/Leap_year#Gregorian_calendar"
42
- }
43
- ],
44
- "tags": [
45
- "date parsing",
46
- "validation",
47
- "scheduling",
48
- "leap year"
49
- ],
50
- "test_cases": [
51
- {
52
- "inputs": [
53
- 1900
54
- ],
55
- "relation": "returns",
56
- "expected": false,
57
- "note": "A correct is_leap_year(1900) must return false."
58
- },
59
- {
60
- "inputs": [
61
- 2000
62
- ],
63
- "relation": "returns",
64
- "expected": true,
65
- "note": "A correct is_leap_year(2000) must return true."
66
- }
67
- ]
68
- }
@@ -1,39 +0,0 @@
1
- {
2
- "id": "datetime-utcnow-naive-deprecated",
3
- "title": "datetime.utcnow() returns a value that is UTC in meaning but naive in type -- and is deprecated as of Python 3.12 because of it",
4
- "category": "datetime",
5
- "assumption": "A function named utcnow() returns a timezone-aware UTC timestamp.",
6
- "input": {
7
- "display": "datetime.utcnow().tzinfo is None",
8
- "description": "Any codebase that used datetime.utcnow() to get 'the current UTC time' and then mixed the result with timezone-aware datetimes elsewhere."
9
- },
10
- "what_goes_wrong": "Code that calls `datetime.utcnow()` expecting a timezone-aware UTC timestamp gets a naive one instead, which then either silently gets treated as local time by something downstream (for example `.timestamp()`, which assumes a naive datetime is local time), or raises `TypeError` the moment it's compared against an aware datetime.",
11
- "why": "`datetime.utcnow()` returns the current UTC time's wall-clock fields but deliberately attaches no tzinfo, making the result naive -- indistinguishable, by type, from a naive datetime that means local time or means nothing in particular. As of Python 3.12 it is deprecated and emits a `DeprecationWarning` (as does its sibling `datetime.utcfromtimestamp()`), because this mismatch between the function's name ('UTC') and its type (naive) has caused enough real bugs; on 3.11 and earlier it runs silently. The documented replacement is `datetime.now(timezone.utc)` (or `datetime.now(datetime.UTC)` on 3.11+), which returns an aware datetime that carries its UTC-ness in the object.",
12
- "affects": [
13
- "python"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "import warnings\nfrom datetime import datetime, timezone\n\nwith warnings.catch_warnings(record=True) as caught:\n warnings.simplefilter(\"always\")\n naive_utc = datetime.utcnow()\n print(\"utcnow() tzinfo:\", naive_utc.tzinfo)\n print(\"warning category raised:\", [w.category.__name__ for w in caught])\n\naware_utc = datetime.now(timezone.utc)\nprint(\"now(timezone.utc) tzinfo:\", aware_utc.tzinfo)\n",
19
- "expected_output": "utcnow() tzinfo: None\nwarning category raised: ['DeprecationWarning']\nnow(timezone.utc) tzinfo: UTC",
20
- "verified_with": "Python 3.14.2"
21
- }
22
- ],
23
- "fix": "Use `datetime.now(timezone.utc)` instead of `datetime.utcnow()`; the result carries its own UTC-ness so it can be safely compared against, and converted from, any other aware datetime.",
24
- "related": [
25
- "python-naive-aware-comparison-typeerror"
26
- ],
27
- "sources": [
28
- {
29
- "title": "Python docs: datetime.utcnow (deprecated since 3.12)",
30
- "url": "https://docs.python.org/3/library/datetime.html#datetime.datetime.utcnow"
31
- }
32
- ],
33
- "tags": [
34
- "timezone",
35
- "timestamps",
36
- "database"
37
- ],
38
- "test_cases": []
39
- }
@@ -1,51 +0,0 @@
1
- {
2
- "id": "dst-fall-back-ambiguous-fold",
3
- "title": "1:30 AM on November 3, 2024 happens twice in New York, and fold picks which one",
4
- "category": "datetime",
5
- "assumption": "A local wall-clock time plus a timezone name uniquely identifies one instant.",
6
- "input": {
7
- "display": "datetime(2024, 11, 3, 1, 30, tzinfo=ZoneInfo(\"America/New_York\"), fold=0) vs fold=1",
8
- "description": "Log timestamps, scheduled job times, or user-entered times that fall in the one-hour window a clock repeats when a timezone falls back off DST."
9
- },
10
- "what_goes_wrong": "Two events both logged as '2024-11-03 01:30 America/New_York' can be a full hour apart in real time, and nothing about the string itself says which.",
11
- "why": "On 2024-11-03 the America/New_York clock reaches 01:59:59 EDT, then repeats back to 01:00:00, now as EST, before continuing to 02:00. The wall-clock hour 01:00-01:59 therefore occurs twice, at two different UTC instants one hour apart. PEP 495 (Python 3.6) added a `fold` attribute to Python's datetime specifically for this: fold=0 (the default) means 'the first occurrence' (-04:00, EDT), fold=1 means 'the second occurrence' (-05:00, EST). Two systems that both construct 01:30 without agreeing on fold silently disagree by exactly one hour.",
12
- "affects": [
13
- "python"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "from zoneinfo import ZoneInfo\nfrom datetime import datetime\n\ntz = ZoneInfo(\"America/New_York\")\nfirst = datetime(2024, 11, 3, 1, 30, tzinfo=tz, fold=0)\nsecond = datetime(2024, 11, 3, 1, 30, tzinfo=tz, fold=1)\nprint(first.utcoffset(), second.utcoffset())\nprint(first.astimezone(ZoneInfo(\"UTC\")))\nprint(second.astimezone(ZoneInfo(\"UTC\")))\n",
19
- "expected_output": "-1 day, 20:00:00 -1 day, 19:00:00\n2024-11-03 05:30:00+00:00\n2024-11-03 06:30:00+00:00",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "// JS Date has no fold concept: the only way to land on either occurrence\n// is to already know the UTC instant. Walking UTC minutes shows local time\n// repeating 01:xx twice with no local-only way to tell which pass you're on.\nconst dtf = new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York', hour12: false, hour: '2-digit', minute: '2-digit' });\nfor (const h of [4, 5, 6]) {\n const utcMs = Date.UTC(2024, 10, 3, h, 30, 0);\n console.log(h + ':30 UTC ->', dtf.format(new Date(utcMs)));\n}\n",
25
- "expected_output": "4:30 UTC -> 00:30\n5:30 UTC -> 01:30\n6:30 UTC -> 01:30",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "Store instants in UTC, never a bare local wall-clock string, especially around DST transitions. If you must accept or log an ambiguous local time, record the UTC offset or the fold/occurrence alongside it, not just the wall-clock string.",
30
- "related": [
31
- "dst-spring-forward-nonexistent-time"
32
- ],
33
- "sources": [
34
- {
35
- "title": "PEP 495 -- Local Time Disambiguation",
36
- "url": "https://peps.python.org/pep-0495/"
37
- },
38
- {
39
- "title": "Python docs: datetime.fold",
40
- "url": "https://docs.python.org/3/library/datetime.html#datetime.datetime.fold"
41
- }
42
- ],
43
- "tags": [
44
- "timezone",
45
- "scheduling",
46
- "timestamps",
47
- "logging",
48
- "date parsing"
49
- ],
50
- "test_cases": []
51
- }
@@ -1,57 +0,0 @@
1
- {
2
- "id": "dst-spring-forward-nonexistent-time",
3
- "title": "2:30 AM on March 10, 2024 never happened in New York, but your code builds it anyway",
4
- "category": "datetime",
5
- "assumption": "Every combination of year/month/day/hour/minute you can type into a date constructor corresponds to a real moment in that timezone.",
6
- "input": {
7
- "display": "datetime(2024, 3, 10, 2, 30, tzinfo=ZoneInfo(\"America/New_York\"))",
8
- "description": "A scheduled reminder, cron-style job, or appointment time entered as a local wall-clock time on the day a timezone springs forward."
9
- },
10
- "what_goes_wrong": "A reminder set for 2:30 AM on the spring-forward day is silently given the pre-transition offset (-05:00), so it fires at 07:30 UTC, which the wall clock shows as 03:30 EDT -- and nothing tells you the time you asked for never existed.",
11
- "why": "On 2024-03-10 in America/New_York, clocks jump from 01:59:59 directly to 03:00:00, so every wall-clock time from 02:00 to 02:59 does not exist locally that day. Python's zoneinfo does not reject the construction. Under PEP 495, a time in a gap with the default fold=0 is interpreted with the offset in effect before the transition, so 02:30 gets -05:00 (EST) and converts to 07:30 UTC, which is 03:30 EDT on the wall clock; fold=1 would use the post-transition offset instead and give 06:30 UTC, which is 01:30 EST. The object itself keeps printing 02:30 until you round-trip it through UTC. JavaScript lands in the same place: ECMA-262 requires a skipped local time to be interpreted with the offset before the transition, so `new Date(2024, 2, 10, 2, 30)` in a process running in America/New_York is also 07:30 UTC. Neither language raises.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "from zoneinfo import ZoneInfo\nfrom datetime import datetime\n\ntz = ZoneInfo(\"America/New_York\")\ngap = datetime(2024, 3, 10, 2, 30, tzinfo=tz)\nprint(gap, gap.utcoffset())\nprint(gap.astimezone(ZoneInfo(\"UTC\")))\n",
20
- "expected_output": "2024-03-10 02:30:00-05:00 -1 day, 19:00:00\n2024-03-10 07:30:00+00:00",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "process.env.TZ = 'America/New_York'; // Node re-reads TZ when it is set at runtime\nconst gap = new Date(2024, 2, 10, 2, 30); // 02:30 local on the spring-forward day\nconsole.log('new Date(2024, 2, 10, 2, 30) ->', gap.toISOString());\nconsole.log('local wall clock:', gap.getHours() + ':' + String(gap.getMinutes()).padStart(2, '0'));\n// Walk UTC minutes through the gap: the wall clock jumps from 01:55 to 03:00.\nconst dtf = new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York', hour12: false, hour: '2-digit', minute: '2-digit' });\nfor (const m of [55, 60]) {\n console.log('06:00 UTC +', m, 'min ->', dtf.format(new Date(Date.UTC(2024, 2, 10, 6, m, 0))));\n}",
26
- "expected_output": "new Date(2024, 2, 10, 2, 30) -> 2024-03-10T07:30:00.000Z\nlocal wall clock: 3:30\n06:00 UTC + 55 min -> 01:55\n06:00 UTC + 60 min -> 03:00",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Store and schedule in UTC; only render to local wall-clock at display time. If you must accept a local wall-clock input, detect the gap explicitly and surface it to the user rather than silently picking a side: with zoneinfo, a time is nonexistent when `dt.astimezone(timezone.utc).astimezone(tz).replace(tzinfo=None) != dt.replace(tzinfo=None)`; with pytz, `tz.localize(naive, is_dst=None)` raises `NonExistentTimeError`.",
31
- "related": [
32
- "dst-fall-back-ambiguous-fold",
33
- "midnight-that-doesnt-exist-sao-paulo"
34
- ],
35
- "sources": [
36
- {
37
- "title": "PEP 495 -- Local Time Disambiguation",
38
- "url": "https://peps.python.org/pep-0495/"
39
- },
40
- {
41
- "title": "Python docs: zoneinfo",
42
- "url": "https://docs.python.org/3/library/zoneinfo.html"
43
- },
44
- {
45
- "title": "IANA Time Zone Database",
46
- "url": "https://www.iana.org/time-zones"
47
- }
48
- ],
49
- "tags": [
50
- "timezone",
51
- "scheduling",
52
- "reminder",
53
- "date parsing",
54
- "timestamps"
55
- ],
56
- "test_cases": []
57
- }
@@ -1,50 +0,0 @@
1
- {
2
- "id": "fractional-utc-offsets-kathmandu-kolkata",
3
- "title": "Not every timezone is a whole number of hours from UTC -- Nepal is 5 hours and 45 minutes off",
4
- "category": "datetime",
5
- "assumption": "A UTC offset is always a whole number of hours (or at worst, whole 30-minute steps).",
6
- "input": {
7
- "display": "ZoneInfo(\"Asia/Kathmandu\") -> UTC+05:45; ZoneInfo(\"Asia/Kolkata\") -> UTC+05:30",
8
- "description": "Scheduling a meeting, computing 'business hours', or bucketing timestamps by local hour for users in Nepal, India, or several Pacific/Indian Ocean zones."
9
- },
10
- "what_goes_wrong": "Code that converts an offset to hours with integer math (`offset_minutes // 60`) or assumes `local_hour = (utc_hour + offset_hours) % 24` is correct silently produces times that are off by 15, 30, or 45 minutes for users in these zones.",
11
- "why": "UTC offsets are defined per timezone by the IANA tz database and are not required to be whole hours. Nepal Standard Time is UTC+05:45, India Standard Time is UTC+05:30, and historically several zones have used offsets like +04:51 or +08:05:43 before standardization. Any code path that models 'timezone' as 'an integer number of hours' or truncates a fractional offset will misplace every timestamp converted through it by the truncated remainder.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "from zoneinfo import ZoneInfo\nfrom datetime import datetime\n\nkathmandu = datetime(2024, 6, 1, 12, 0, tzinfo=ZoneInfo(\"Asia/Kathmandu\"))\nkolkata = datetime(2024, 6, 1, 12, 0, tzinfo=ZoneInfo(\"Asia/Kolkata\"))\nprint(\"Kathmandu offset:\", kathmandu.utcoffset())\nprint(\"Kolkata offset:\", kolkata.utcoffset())\nprint(\"naive hour-only offset would truncate to:\", kathmandu.utcoffset().seconds // 3600, \"hours, losing the 45 minutes\")\n",
20
- "expected_output": "Kathmandu offset: 5:45:00\nKolkata offset: 5:30:00\nnaive hour-only offset would truncate to: 5 hours, losing the 45 minutes",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "function offsetMinutes(utcMs, timeZone) {\n const dtf = new Intl.DateTimeFormat('en-US', { timeZone, hour12: false,\n year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit', second:'2-digit' });\n const parts = dtf.formatToParts(new Date(utcMs));\n const o = {};\n for (const p of parts) o[p.type] = p.value;\n const asUTC = Date.UTC(+o.year, +o.month - 1, +o.day, +o.hour, +o.minute, +o.second);\n return (asUTC - utcMs) / 60000;\n}\nconst t = Date.UTC(2024, 5, 1, 12, 0, 0);\nconsole.log('Kathmandu offset minutes:', offsetMinutes(t, 'Asia/Kathmandu'));\nconsole.log('Kolkata offset minutes:', offsetMinutes(t, 'Asia/Kolkata'));\n",
26
- "expected_output": "Kathmandu offset minutes: 345\nKolkata offset minutes: 330",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Always carry UTC offsets (and do timezone conversion) in minutes or seconds, never truncated hours, and let the timezone library (zoneinfo, Intl) compute the offset rather than hand-rolling it from a hours table.",
31
- "related": [
32
- "lord-howe-30-minute-dst"
33
- ],
34
- "sources": [
35
- {
36
- "title": "IANA Time Zone Database",
37
- "url": "https://www.iana.org/time-zones"
38
- },
39
- {
40
- "title": "Wikipedia: list of UTC offsets (background, not sole source)",
41
- "url": "https://en.wikipedia.org/wiki/List_of_UTC_offsets"
42
- }
43
- ],
44
- "tags": [
45
- "timezone",
46
- "scheduling",
47
- "date parsing"
48
- ],
49
- "test_cases": []
50
- }
@@ -1,48 +0,0 @@
1
- {
2
- "id": "iso-week-year-vs-calendar-year",
3
- "title": "December 30, 2024 is in ISO week 1 of 2025, not week 52 of 2024",
4
- "category": "datetime",
5
- "assumption": "The 'year' a date belongs to for week-numbering purposes is just its calendar year.",
6
- "input": {
7
- "display": "date(2024, 12, 30).isocalendar() == (2025, 1, 1) -- ISO week-year 2025, week 1, weekday Monday",
8
- "description": "Any report, invoice numbering, or analytics bucketing that labels data by 'year-week' (e.g. '2024-W52') using the calendar year instead of the ISO week-year."
9
- },
10
- "what_goes_wrong": "A 'weekly report for year X, week Y' system that pairs the calendar year with the ISO week number produces a nonsensical or wrong label like '2024-W01' for a date that is actually in the first ISO week of 2025, or silently double-counts/skips a week at the year boundary.",
11
- "why": "ISO 8601 defines week 1 of a year as the week containing that year's first Thursday (equivalently, the week containing January 4th), and weeks run Monday-Sunday. Because of this rule, the last few days of December can fall into week 1 of the FOLLOWING year (as with 2024-12-30, a Monday, which starts the week containing 2025-01-02, a Thursday), and conversely the first few days of January can belong to the last week of the PREVIOUS year. The ISO week-year (`%G` in strftime, `isocalendar()[0]` in Python) is therefore a distinct value from the plain calendar year (`%Y`), and the two disagree exactly in this boundary zone.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "from datetime import date\n\nd = date(2024, 12, 30)\nprint(\"calendar year:\", d.year)\nprint(\"isocalendar:\", d.isocalendar())\nprint(d.strftime(\"%G-W%V\"), \"vs plain %Y:\", d.strftime(\"%Y\"))\n",
20
- "expected_output": "calendar year: 2024\nisocalendar: datetime.IsoCalendarDate(year=2025, week=1, weekday=1)\n2025-W01 vs plain %Y: 2024",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "// JavaScript has no built-in ISO week function; this is the standard\n// ISO 8601 algorithm (Thursday of the week decides the week-year).\nfunction isoWeekInfo(y, m, day) {\n const d = new Date(Date.UTC(y, m, day));\n const dayNum = (d.getUTCDay() + 6) % 7; // Monday=0..Sunday=6\n d.setUTCDate(d.getUTCDate() - dayNum + 3); // Thursday of this week\n const isoYear = d.getUTCFullYear();\n const jan4 = new Date(Date.UTC(isoYear, 0, 4));\n const week = 1 + Math.round(((d - jan4) / 86400000 - 3 + ((jan4.getUTCDay() + 6) % 7)) / 7);\n return { isoYear, week };\n}\nconsole.log('calendar year:', 2024, '| ISO:', isoWeekInfo(2024, 11, 30));\n",
26
- "expected_output": "calendar year: 2024 | ISO: { isoYear: 2025, week: 1 }",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Always use the ISO week-year (`%G`), never the plain calendar year (`%Y`), when labeling data with an ISO week number (`%V`); most date libraries expose both separately specifically because they can diverge.",
31
- "related": [],
32
- "sources": [
33
- {
34
- "title": "ISO 8601 week date (Wikipedia overview, verify against ISO 8601:2004)",
35
- "url": "https://en.wikipedia.org/wiki/ISO_week_date"
36
- },
37
- {
38
- "title": "Python docs: strftime %G/%V format codes",
39
- "url": "https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes"
40
- }
41
- ],
42
- "tags": [
43
- "date parsing",
44
- "database",
45
- "validation"
46
- ],
47
- "test_cases": []
48
- }
@@ -1,49 +0,0 @@
1
- {
2
- "id": "js-date-field-overflow-rollover",
3
- "title": "new Date(2024, 1, 30) silently becomes March 1, not an error",
4
- "category": "datetime",
5
- "assumption": "Passing an out-of-range day (or month) to a date constructor either gets rejected or clamped to the nearest valid value.",
6
- "input": {
7
- "display": "new Date(2024, 1, 30) -- February only has 29 days in 2024",
8
- "description": "Date arithmetic done by hand on year/month/day components, e.g. 'last day of next month minus a few days', or a date picker that lets a day number outlive the month the user is also changing."
9
- },
10
- "what_goes_wrong": "Code computing a date by incrementing a day-of-month field without checking the month's length doesn't throw and doesn't clamp -- it quietly rolls over into the next month entirely, so 'February 30th' becomes 'March 1st' with no signal anything went wrong.",
11
- "why": "The ECMA-262 Date constructor specifies that out-of-range field values are normalized by carrying the excess into the next larger field, rather than being validated and rejected. Day 30 is one past February's last day in 2024 (the 29th, since 2024 is a leap year), so the extra day carries into March and the result is March 1. The same arithmetic in Python's `date()` constructor raises `ValueError` instead, because Python validates each field against the actual calendar rather than normalizing it.",
12
- "affects": [
13
- "javascript"
14
- ],
15
- "repros": [
16
- {
17
- "language": "javascript",
18
- "code": "const d = new Date(2024, 1, 30); // month 1 = February (0-indexed), day 30\nconsole.log(d.getFullYear(), d.getMonth(), d.getDate());\n// Also overflows the month field itself:\nconst d2 = new Date(2024, 13, 1); // month 13 doesn't exist\nconsole.log(d2.getFullYear(), d2.getMonth(), d2.getDate());\n",
19
- "expected_output": "2024 2 1\n2025 1 1",
20
- "verified_with": "Node.js v22.22.2"
21
- },
22
- {
23
- "language": "python",
24
- "code": "from datetime import date\n\ntry:\n date(2024, 2, 30)\nexcept ValueError as e:\n print(\"Python raises instead of rolling over:\", e)\n",
25
- "expected_output": "Python raises instead of rolling over: day 30 must be in range 1..29 for month 2 in year 2024",
26
- "verified_with": "Python 3.14.2"
27
- }
28
- ],
29
- "fix": "Never construct a Date from hand-computed year/month/day components without first validating the day against that month's actual length (and the month against 0-11); if you want 'last valid day of the month' semantics, compute it explicitly rather than relying on overflow behavior.",
30
- "related": [
31
- "century-leap-year-1900-not-leap",
32
- "age-calculator-feb29-birthday-crash-vs-silent"
33
- ],
34
- "sources": [
35
- {
36
- "title": "ECMA-262: Date constructor / MakeDay normalization",
37
- "url": "https://tc39.es/ecma262/#sec-date-constructor"
38
- },
39
- {
40
- "title": "MDN: Date() constructor",
41
- "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date"
42
- }
43
- ],
44
- "tags": [
45
- "date parsing",
46
- "validation"
47
- ],
48
- "test_cases": []
49
- }
@@ -1,57 +0,0 @@
1
- {
2
- "id": "leap-second-rejected-vs-silently-invalid",
3
- "title": "A ':60' leap-second timestamp raises in Python, is an Invalid Date in JS -- and V8's fallback parser silently turns it into 23:59:00",
4
- "category": "datetime",
5
- "assumption": "A timestamp string like 2016-12-31T23:59:60Z, which is how official leap-second records are written, is a value your date library can parse.",
6
- "input": {
7
- "display": "\"2016-12-31T23:59:60Z\" (the real UTC leap second inserted at the end of 2016)",
8
- "description": "Parsing a timestamp from an NTP/GPS log, a scientific instrument feed, or any system that records UTC leap seconds literally in its output, where one legitimately occurred (e.g. 2016-12-31 23:59:60 UTC)."
9
- },
10
- "what_goes_wrong": "Feeding a real, historically-occurring leap-second timestamp to a standard date parser fails three different ways: loudly in Python, as a silently invalid Date for an ISO string in JavaScript, and, for a non-ISO string in V8 (Node, Chrome), as a valid-looking time one minute too early.",
11
- "why": "Python's `datetime` and the ECMAScript date-time string format both model a minute as seconds 00 through 59, with no slot for the 61st second a leap second inserts. In Python, `datetime(..., second=60)`, `datetime.strptime` and `datetime.fromisoformat` all raise `ValueError` (the older `time.strptime`, by contrast, accepts 60). ECMA-262 requires `Date.parse` to return `NaN` for a string in its ISO format with an out-of-bounds field, so `\"2016-12-31T23:59:60Z\"` yields an Invalid Date: no exception, every <, >, <=, >= comparison with it is false, and `.toISOString()` throws `RangeError` only later, at the point of use. Strings outside the ISO format go to implementation-defined fallback parsing, and there V8 accepts `\"Sat, 31 Dec 2016 23:59:60 GMT\"` and returns 2016-12-31T23:59:00Z -- a valid Date 60 seconds early, with no error at all. Other engines may differ, since that fallback is not specified.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "from datetime import datetime\n\ntry:\n datetime(2016, 12, 31, 23, 59, 60)\nexcept ValueError as e:\n print(\"ValueError at construction:\", e)\n",
20
- "expected_output": "ValueError at construction: second must be in 0..59, not 60",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "const ms = Date.parse(\"2016-12-31T23:59:60Z\");\nconsole.log(\"Date.parse result:\", ms, \"isNaN:\", Number.isNaN(ms));\nconst d = new Date(\"2016-12-31T23:59:60Z\");\nconsole.log(\"Date object:\", d.toString(), \"| valid:\", !Number.isNaN(d.getTime()));\n// Non-ISO (RFC 2822 style) string: implementation-defined fallback parser.\nconst rfc = new Date(\"Sat, 31 Dec 2016 23:59:60 GMT\");\nconsole.log(\"V8 fallback parse of RFC 2822 ':60':\", rfc.toISOString());",
26
- "expected_output": "Date.parse result: NaN isNaN: true\nDate object: Invalid Date | valid: false\nV8 fallback parse of RFC 2822 ':60': 2016-12-31T23:59:00.000Z",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Never parse a raw leap-second timestamp with the standard date constructor. Detect `:60` (or `:59.999...` smeared variants, depending on the source) before parsing and either clamp it to `:59` with a documented policy or route it through a library that explicitly models leap seconds (e.g. one backed by TAI); and in JavaScript, always check `Number.isNaN(date.getTime())` immediately after any `Date.parse`/`new Date(string)` rather than trusting the object; and never hand a non-ISO timestamp string to `new Date()` / `Date.parse`, whose fallback parsing is engine-specific.",
31
- "related": [],
32
- "sources": [
33
- {
34
- "title": "IETF RFC 3339: Date and Time on the Internet (leap seconds)",
35
- "url": "https://www.rfc-editor.org/rfc/rfc3339#section-5.7"
36
- },
37
- {
38
- "title": "ECMA-262: Date Time String Format",
39
- "url": "https://tc39.es/ecma262/#sec-date-time-string-format"
40
- },
41
- {
42
- "title": "Python docs: datetime",
43
- "url": "https://docs.python.org/3/library/datetime.html"
44
- },
45
- {
46
- "title": "ECMA-262: Date.parse (non-ISO strings are implementation-defined; out-of-bounds ISO fields return NaN)",
47
- "url": "https://tc39.es/ecma262/#sec-date.parse"
48
- }
49
- ],
50
- "tags": [
51
- "date parsing",
52
- "timestamps",
53
- "validation",
54
- "logging"
55
- ],
56
- "test_cases": []
57
- }