Skip to content

fix(business-days): stop the day walk from hanging in five time zones, and show the month recipe - #561

Open
hyanmandian wants to merge 4 commits into
claude/gtinfrom
claude/business-day-helpers
Open

hyanmandian wants to merge 4 commits into
claude/gtinfrom
claude/business-day-helpers

Conversation

@hyanmandian

@hyanmandian hyanmandian commented Sep 19, 2026 •

Copy link
Copy Markdown
Member

Stacked on #563. This PR sits on top of #563 (isValidGtin/getGtinInfo) and merges after it. Its base branch is claude/gtin, so the diff shown here is the business day change alone.

What

It fixes a hang in released code: addBusinessDays, subBusinessDays and differenceInBusinessDays could loop forever in five time zones. It also shows, in the docs and in tests, how to get the n-th and the last business day of a month with the utils that already exist.

This PR first proposed getNthBusinessDay, getNextBusinessDay and getLastBusinessDayOfMonth. They were dropped (and removed from the history, so no feat commit reaches the changelog) because the existing utils already answer those questions:

Question With the existing utils
next business day after d addBusinessDays(d, 1) (that is all getNextBusinessDay did)
n-th business day of a month addBusinessDays(new Date(y, m, 0), n), from the last day of the month before
last business day of a month subBusinessDays(new Date(y, m + 1, 1), 1), from the first day of the month after

The one real difference, an n beyond the business days of the month landing in the next month instead of null, is documented next to the recipe (compare getMonth()), together with the year edges (January 1900 and December 2099 return null, since the starting day is outside the supported years).

The fix to the released utils (fix(business-days))

The walk advanced with result.setDate(result.getDate() + step), which is not guaranteed to change the local calendar day. When the neighbouring local day does not exist, because the zone skipped it to cross the date line, the runtime re-normalizes onto the same day and the loop has a fixed point: it never terminates and freezes the calling thread (a browser tab with it) on valid input.

Zone Local day that does not exist
Pacific/Apia 30 December 2011
Pacific/Fakaofo 30 December 2011
Pacific/Kiritimati 31 December 1994
Pacific/Enderbury 31 December 1994
Pacific/Kwajalein 21 August 1993
TZ=Pacific/Apia  subBusinessDays(new Date(2012, 0, 5, 12), 4)                              never returns
TZ=Pacific/Apia  addBusinessDays(new Date(2012, 0, 5, 12), -4)                             never returns
TZ=Pacific/Apia  differenceInBusinessDays(new Date(2011, 11, 1), new Date(2011, 11, 31))   never returns

The same normalization also dragged a shifted hour through the walk: a walk crossing the half hour transition of Australia/Lord_Howe moved 02:15 to 02:45.

The new internal src/_internals/each-local-day walks the days between two local calendar days with an integer counter bounded by the distance, so it always advances and always terminates; every day is yielded at noon (the one time every existing local day has); the five missing days are skipped rather than visited twice. addBusinessDays (and subBusinessDays) and differenceInBusinessDays share it. fix(get-holidays) memoizes the local day rather than the instant it was built at.

Behaviour is unchanged for every input that did not hang or fall on a daylight saving boundary; the whole existing suite passes untouched.

The month recipe (docs(business-days))

Both docs/utilities.md files show the recipe under subBusinessDays. It is pinned by tests in add-business-days.test.ts and sub-business-days.test.ts: hand counted examples (Ano novo, Carnaval with and without includeOptional, Sexta-feira Santa, Corpus Christi, a state holiday, the month spill, the year edges, December 2011 in Pacific/Apia, the case that used to hang) and fast-check properties comparing the recipe with a brute force walk of the month through isBusinessDay, generated by a new businessDayMonths arbitrary.

The payroll "quinto dia útil" of CLT art. 459 § 1º is counted with Saturdays by labour inspection; #573, on top of this PR, adds includeSaturday for that and extends the recipe to it.

The docs name the recipe's year edges precisely (the n-th business day of January 1900 and the last of December 2099), subBusinessDays documents the same time-of-day caveat as addBusinessDays, and the day walk comment lists Pacific/Fakaofo with the other four zones.

Verification

  • npm run check: pass
  • npx vp test run --coverage: 6567 passed, 100% statements, branches, functions and lines
  • npm run check:unused, npm run check:duplication (0 clones), npm run check:api (no public change): pass
  • npx commitlint --from origin/main --to HEAD: 0 problems

🤖 Generated with Claude Code

https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH

@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 2098c59f-f180-4dba-80a3-376ec3066594

📥 Commits

Reviewing files that changed from the base of the PR and between 4a29787 and a2c1588.

📒 Files selected for processing (8)
  • docs/pt-br/utilities.md
  • docs/utilities.md
  • jsr.json
  • reports/api/brazilian-utils.api.md
  • src/get-holidays/get-holidays.test.ts
  • src/get-holidays/get-holidays.ts
  • src/index.test.ts
  • src/index.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

Changes

Business-day utilities

Layer / File(s) Summary
Local-day traversal and timezone test support
src/_internals/each-local-day/*, src/_internals/test/*
Adds timezone-safe local-day iteration and test helpers for runtime timezone support.
Existing calculation migration
src/add-business-days/*, src/difference-in-business-days/*, src/sub-business-days/*, src/get-holidays/*
Updates business-day calculations to use local-day traversal and stores holiday cache entries as calendar components.
New business-day APIs
src/get-nth-business-day/*, src/get-next-business-day/*, src/get-last-business-day-of-month/*
Adds the three APIs with validation, holiday options, supported-year limits, nullable results, and timezone coverage.
Public surface and documentation
src/index.*, jsr.json, reports/api/*, docs/utilities.md, docs/pt-br/utilities.md
Exports the APIs and documents their signatures and behavior.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant getNextBusinessDay
  participant addBusinessDays
  participant eachLocalDay
  participant isBusinessDay
  Caller->>getNextBusinessDay: provide date and options
  getNextBusinessDay->>addBusinessDays: request next business day
  addBusinessDays->>eachLocalDay: iterate local calendar days
  eachLocalDay-->>addBusinessDays: yield representable day
  addBusinessDays->>isBusinessDay: evaluate holidays and weekdays
  isBusinessDay-->>addBusinessDays: return business-day status
  addBusinessDays-->>Caller: return Date or null
Loading

Merge Risk: ⚪ Minimal · up to a2c15

The PR adds timezone-safe business-day APIs and updates existing calculations with documented exports and coverage. It is ready to merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 19 files. (4 skipped: 4… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the timezone day-walk fixes and the added month-based business-day documentation. These are major parts of the pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 19 files. (4 skipped: 4 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Tree-shaking report

✅ No size regression. 6 grew out of 166 exports.

Base Head Δ
Pre-existing exports, all imported 888.7 KB 889.2 KB (gzip 217.2 KB) +542 B (+0.1%)
Full import 888.7 KB 889.2 KB (gzip 217.2 KB) +542 B (+0.1%)
Exports 166 166 0

What changed (6)

Export Base Head Δ gzip
🟡 addBusinessDays 6.8 KB 7.4 KB +596 B (+8.5%) 3.0 KB
🟡 subBusinessDays 6.9 KB 7.5 KB +596 B (+8.4%) 3.1 KB
🟡 differenceInBusinessDays 6.9 KB 7.2 KB +351 B (+5.0%) 3.0 KB
🟡 getHolidays 6.1 KB 6.3 KB +158 B (+2.5%) 2.6 KB
🟡 isBusinessDay 6.5 KB 6.7 KB +158 B (+2.4%) 2.8 KB
🟡 isHoliday 6.4 KB 6.6 KB +158 B (+2.4%) 2.7 KB
All exports (166)
Export Base Head Δ gzip
⚪ GetAddressInfoByCepError 966 B 966 B 0 B 600 B
⚪ GetAddressInfoByCepNotFoundError 1.0 KB 1.0 KB 0 B 618 B
⚪ GetAddressInfoByCepServiceError 1.0 KB 1.0 KB 0 B 617 B
⚪ GetAddressInfoByCepValidationError 1.0 KB 1.0 KB 0 B 620 B
⚪ GetCepInfoByAddressError 966 B 966 B 0 B 600 B
⚪ GetCepInfoByAddressNotFoundError 1.0 KB 1.0 KB 0 B 618 B
⚪ GetCepInfoByAddressValidationError 1.0 KB 1.0 KB 0 B 620 B
🟡 addBusinessDays 6.8 KB 7.4 KB +596 B (+8.5%) 3.0 KB
⚪ capitalize 2.5 KB 2.5 KB 0 B 1.3 KB
⚪ convertCurrencyToWords 2.8 KB 2.8 KB 0 B 1.5 KB
⚪ convertDateToWords 3.2 KB 3.2 KB 0 B 1.7 KB
⚪ convertLicensePlateToMercosul 1.3 KB 1.3 KB 0 B 807 B
⚪ convertNumberToWords 2.4 KB 2.4 KB 0 B 1.3 KB
🟡 differenceInBusinessDays 6.9 KB 7.2 KB +351 B (+5.0%) 3.0 KB
⚪ formatBoleto 1.4 KB 1.4 KB 0 B 837 B
⚪ formatCEP 1.2 KB 1.2 KB 0 B 777 B
⚪ formatCNPJ 1.4 KB 1.4 KB 0 B 854 B
⚪ formatCPF 1.3 KB 1.3 KB 0 B 806 B
⚪ formatCaepf 1.3 KB 1.3 KB 0 B 786 B
⚪ formatCei 1.3 KB 1.3 KB 0 B 785 B
⚪ formatCep 1.2 KB 1.2 KB 0 B 777 B
⚪ formatCertidao 1.3 KB 1.3 KB 0 B 789 B
⚪ formatCnae 1.2 KB 1.2 KB 0 B 781 B
⚪ formatCnh 1.3 KB 1.3 KB 0 B 780 B
⚪ formatCno 1.3 KB 1.3 KB 0 B 786 B
⚪ formatCnpj 1.4 KB 1.4 KB 0 B 854 B
⚪ formatCns 1.3 KB 1.3 KB 0 B 780 B
⚪ formatCpf 1.3 KB 1.3 KB 0 B 806 B
⚪ formatCurrency 1.8 KB 1.8 KB 0 B 1.0 KB
⚪ formatIban 1.1 KB 1.1 KB 0 B 696 B
⚪ formatLegalNature 1.2 KB 1.2 KB 0 B 777 B
⚪ formatLicensePlate 1.2 KB 1.2 KB 0 B 738 B
⚪ formatNcm 1.2 KB 1.2 KB 0 B 780 B
⚪ formatNfeKey 1.3 KB 1.3 KB 0 B 783 B
⚪ formatPassport 1.0 KB 1.0 KB 0 B 643 B
⚪ formatPhone 2.8 KB 2.8 KB 0 B 1.4 KB
⚪ formatPis 1.3 KB 1.3 KB 0 B 781 B
⚪ formatProcessoJuridico 1.3 KB 1.3 KB 0 B 785 B
⚪ formatSuframa 1.3 KB 1.3 KB 0 B 779 B
⚪ formatVoterId 1.3 KB 1.3 KB 0 B 821 B
⚪ generateBoleto 2.1 KB 2.1 KB 0 B 1.2 KB
⚪ generateCNPJ 1.6 KB 1.6 KB 0 B 967 B
⚪ generateCPF 1.4 KB 1.4 KB 0 B 878 B
⚪ generateCep 984 B 984 B 0 B 609 B
⚪ generateCnh 1.4 KB 1.4 KB 0 B 829 B
⚪ generateCnpj 1.6 KB 1.6 KB 0 B 967 B
⚪ generateCpf 1.4 KB 1.4 KB 0 B 878 B
⚪ generateLegalNature 5.9 KB 5.9 KB 0 B 2.1 KB
⚪ generateLicensePlate 1.1 KB 1.1 KB 0 B 692 B
⚪ generatePassport 1.1 KB 1.1 KB 0 B 655 B
⚪ generatePhone 1.5 KB 1.5 KB 0 B 900 B
⚪ generatePis 1.2 KB 1.2 KB 0 B 743 B
⚪ generatePixPayload 6.3 KB 6.3 KB 0 B 2.8 KB
⚪ generateProcessoJuridico 1.4 KB 1.4 KB 0 B 870 B
⚪ generateRenavam 1.2 KB 1.2 KB 0 B 760 B
⚪ generateSuframa 1.3 KB 1.3 KB 0 B 809 B
⚪ generateVoterId 1.7 KB 1.7 KB 0 B 1022 B
⚪ getAddressInfoByCep 4.1 KB 4.1 KB 0 B 1.9 KB
⚪ getAreaCodeInfo 3.9 KB 3.9 KB 0 B 1.4 KB
⚪ getAreaCodesByState 1.6 KB 1.6 KB 0 B 918 B
⚪ getBankByCode 38.6 KB 38.6 KB 0 B 9.8 KB
⚪ getBankByIspb 38.6 KB 38.6 KB 0 B 9.8 KB
⚪ getBanks 38.4 KB 38.4 KB 0 B 9.6 KB
⚪ getBoletoInfo 3.1 KB 3.1 KB 0 B 1.6 KB
⚪ getCbo 119.1 KB 119.1 KB 0 B 30.7 KB
⚪ getCepInfoByAddress 2.7 KB 2.7 KB 0 B 1.4 KB
⚪ getCertidaoInfo 1.8 KB 1.8 KB 0 B 1.0 KB
⚪ getCfop 68.9 KB 68.9 KB 0 B 6.9 KB
⚪ getCities 154.3 KB 154.3 KB 0 B 49.9 KB
⚪ getCnae 93.9 KB 93.9 KB 0 B 21.2 KB
⚪ getCnpjInfo 1.8 KB 1.8 KB 0 B 1010 B
⚪ getCpfInfo 1.7 KB 1.7 KB 0 B 999 B
⚪ getFormatLicensePlate 1.1 KB 1.1 KB 0 B 691 B
⚪ getGtinInfo 1.7 KB 1.7 KB 0 B 1.0 KB
🟡 getHolidays 6.1 KB 6.3 KB +158 B (+2.5%) 2.6 KB
⚪ getIbanInfo 1.6 KB 1.6 KB 0 B 955 B
⚪ getLegalNature 6.3 KB 6.3 KB 0 B 2.3 KB
⚪ getLegalNatures 5.9 KB 5.9 KB 0 B 2.1 KB
⚪ getLegalNaturesByCategory 6.5 KB 6.5 KB 0 B 2.4 KB
⚪ getMunicipalities 156.4 KB 156.4 KB 0 B 50.3 KB
⚪ getMunicipality 154.9 KB 154.9 KB 0 B 50.3 KB
⚪ getMunicipalityByCep 393.3 KB 393.3 KB 0 B 98.8 KB
⚪ getMunicipalityByCode 156.5 KB 156.5 KB 0 B 50.4 KB
⚪ getNfeKeyInfo 2.7 KB 2.7 KB 0 B 1.5 KB
⚪ getPixKeyInfo 4.5 KB 4.5 KB 0 B 2.0 KB
⚪ getPixPayloadInfo 2.9 KB 2.9 KB 0 B 1.4 KB
⚪ getStateByCep 4.5 KB 4.5 KB 0 B 1.5 KB
⚪ getStateByIbgeCode 3.2 KB 3.2 KB 0 B 1.1 KB
⚪ getStateCodeByName 3.2 KB 3.2 KB 0 B 1.1 KB
⚪ getStateNameByCode 3.1 KB 3.1 KB 0 B 1.0 KB
⚪ getStates 3.0 KB 3.0 KB 0 B 1019 B
⚪ getTimezoneByState 1.6 KB 1.6 KB 0 B 809 B
🟡 isBusinessDay 6.5 KB 6.7 KB +158 B (+2.4%) 2.8 KB
🟡 isHoliday 6.4 KB 6.6 KB +158 B (+2.4%) 2.7 KB
⚪ isValidBankAccount 7.4 KB 7.4 KB 0 B 2.9 KB
⚪ isValidBoleto 2.4 KB 2.4 KB 0 B 1.3 KB
⚪ isValidCEP 984 B 984 B 0 B 610 B
⚪ isValidCNPJ 1.6 KB 1.6 KB 0 B 912 B
⚪ isValidCPF 1.3 KB 1.3 KB 0 B 805 B
⚪ isValidCaepf 1.5 KB 1.5 KB 0 B 912 B
⚪ isValidCbo 119.2 KB 119.2 KB 0 B 30.7 KB
⚪ isValidCei 1.5 KB 1.5 KB 0 B 899 B
⚪ isValidCep 984 B 984 B 0 B 610 B
⚪ isValidCertidao 1.6 KB 1.6 KB 0 B 938 B
⚪ isValidCfop 68.9 KB 68.9 KB 0 B 6.9 KB
⚪ isValidCnae 94.0 KB 94.0 KB 0 B 21.2 KB
⚪ isValidCnh 1.4 KB 1.4 KB 0 B 856 B
⚪ isValidCno 1.5 KB 1.5 KB 0 B 901 B
⚪ isValidCnpj 1.6 KB 1.6 KB 0 B 912 B
⚪ isValidCns 1.5 KB 1.5 KB 0 B 925 B
⚪ isValidCpf 1.3 KB 1.3 KB 0 B 805 B
⚪ isValidCreditCard 1.4 KB 1.4 KB 0 B 896 B
⚪ isValidCsosn 1.2 KB 1.2 KB 0 B 737 B
⚪ isValidCst 1.8 KB 1.8 KB 0 B 1.0 KB
⚪ isValidEmail 1.0 KB 1.0 KB 0 B 622 B
⚪ isValidGtin 1.3 KB 1.3 KB 0 B 825 B
⚪ isValidIE 5.7 KB 5.7 KB 0 B 2.2 KB
⚪ isValidIban 1.3 KB 1.3 KB 0 B 836 B
⚪ isValidIe 5.7 KB 5.7 KB 0 B 2.2 KB
⚪ isValidLandlinePhone 1.5 KB 1.5 KB 0 B 932 B
⚪ isValidLegalNature 5.8 KB 5.8 KB 0 B 2.1 KB
⚪ isValidLicensePlate 1.1 KB 1.1 KB 0 B 702 B
⚪ isValidMobilePhone 1.6 KB 1.6 KB 0 B 971 B
⚪ isValidNcm 114.2 KB 114.2 KB 0 B 24.6 KB
⚪ isValidNfeKey 2.7 KB 2.7 KB 0 B 1.5 KB
⚪ isValidPIS 1.2 KB 1.2 KB 0 B 785 B
⚪ isValidPassport 1.0 KB 1.0 KB 0 B 654 B
⚪ isValidPhone 2.6 KB 2.6 KB 0 B 1.3 KB
⚪ isValidPis 1.2 KB 1.2 KB 0 B 785 B
⚪ isValidPixKey 4.6 KB 4.6 KB 0 B 2.1 KB
⚪ isValidPixPayload 2.9 KB 2.9 KB 0 B 1.5 KB
⚪ isValidProcessoJuridico 1.3 KB 1.3 KB 0 B 787 B
⚪ isValidRegistroProfissional 1.6 KB 1.6 KB 0 B 964 B
⚪ isValidRenavam 1.3 KB 1.3 KB 0 B 815 B
⚪ isValidServicePhone 1.5 KB 1.5 KB 0 B 845 B
⚪ isValidSuframa 1.4 KB 1.4 KB 0 B 883 B
⚪ isValidVin 1.6 KB 1.6 KB 0 B 995 B
⚪ isValidVoterId 1.6 KB 1.6 KB 0 B 900 B
⚪ parseBoleto 1020 B 1020 B 0 B 634 B
⚪ parseCaepf 1003 B 1003 B 0 B 621 B
⚪ parseCbo 1002 B 1002 B 0 B 620 B
⚪ parseCei 1003 B 1003 B 0 B 619 B
⚪ parseCep 1002 B 1002 B 0 B 620 B
⚪ parseCertidao 1003 B 1003 B 0 B 620 B
⚪ parseCfop 1002 B 1002 B 0 B 620 B
⚪ parseCnae 1002 B 1002 B 0 B 620 B
⚪ parseCnh 1003 B 1003 B 0 B 621 B
⚪ parseCno 1003 B 1003 B 0 B 619 B
⚪ parseCnpj 1.1 KB 1.1 KB 0 B 668 B
⚪ parseCns 1003 B 1003 B 0 B 621 B
⚪ parseCpf 1003 B 1003 B 0 B 621 B
⚪ parseCurrency 1.4 KB 1.4 KB 0 B 881 B
⚪ parseIban 1.0 KB 1.0 KB 0 B 638 B
⚪ parseLegalNature 1002 B 1002 B 0 B 620 B
⚪ parseLicensePlate 1.0 KB 1.0 KB 0 B 638 B
⚪ parseNcm 1002 B 1002 B 0 B 620 B
⚪ parseNfeKey 1.0 KB 1.0 KB 0 B 658 B
⚪ parsePassport 1.0 KB 1.0 KB 0 B 637 B
⚪ parsePhone 1.1 KB 1.1 KB 0 B 707 B
⚪ parsePis 1003 B 1003 B 0 B 621 B
⚪ parseProcessoJuridico 1003 B 1003 B 0 B 621 B
⚪ parseSuframa 1002 B 1002 B 0 B 620 B
⚪ parseVoterId 1.0 KB 1.0 KB 0 B 649 B
⚪ removeAccents 953 B 953 B 0 B 593 B
🟡 subBusinessDays 6.9 KB 7.5 KB +596 B (+8.4%) 3.1 KB
⚪ toStandardSchema 1.1 KB 1.1 KB 0 B 714 B
How this is measured

Every export is imported alone into an esbuild consumer bundle (minified, tree-shaken) built from the head and from the base of this pull request; the sizes are the resulting bundles, gzip is their gzipped size. 🔴 marks a regression: a pre-existing export that grew more than 20% and more than 256 B, or the bundle importing every pre-existing export growing more than 5%. 🟡 is growth under the threshold, 🟢 a decrease, ⚪ no change, 🆕 an export that does not exist on the base (never a regression), 🗑️ an export that was removed. An intentional increase is accepted with the tree-shaking: accepted label.

@codecov

codecov Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (6c00fd1) to head (464c33d).

Additional details and impacted files
@@              Coverage Diff              @@
##           claude/gtin      #561   +/-   ##
=============================================
  Coverage       100.00%   100.00%           
=============================================
  Files              197       198    +1     
  Lines             2137      2145    +8     
  Branches           630       632    +2     
=============================================
+ Hits              2137      2145    +8     
Flag Coverage Δ
node 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@hyanmandian hyanmandian changed the title feat(business-days): add getNthBusinessDay, getNextBusinessDay and getLastBusinessDayOfMonth feat(business-days): add getNthBusinessDay, getNextBusinessDay and getLastBusinessDayOfMonth, and fix the day walk that hangs in five time zones Sep 19, 2026
@hyanmandian

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@hyanmandian
hyanmandian added this pull request to stack #577 September 19, 2026 19:30
@hyanmandian
hyanmandian removed this pull request from stack #577 September 22, 2026 05:37
@hyanmandian
hyanmandian force-pushed the claude/business-day-helpers branch from 4a29787 to 84304c3 Compare September 22, 2026 05:40
@hyanmandian
hyanmandian changed the base branch from main to claude/gtin September 22, 2026 05:40
@vercel

vercel Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
brazilian-utils Ready Ready Preview Sep 26, 2026 7:01pm UTC

@pkg-pr-new

pkg-pr-new Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@brazilian-utils/brazilian-utils@561

commit: 464c33d

@hyanmandian

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

hyanmandian and others added 4 commits September 26, 2026 18:54
…utating a Date

addBusinessDays, subBusinessDays and differenceInBusinessDays advanced their
walk with result.setDate(result.getDate() + step). That call is not guaranteed
to change the local calendar day: when the neighbouring day does not exist in
the zone, because it was skipped to cross the date line, the runtime
re-normalizes onto the same day and the loop has a fixed point. Under
TZ=Pacific/Apia, subBusinessDays(new Date(2012, 0, 5, 12), 4) and
differenceInBusinessDays(new Date(2011, 11, 1), new Date(2011, 11, 31)) never
return: they freeze the calling thread, and a browser tab with it, on input
that is perfectly valid. Pacific/Fakaofo (30 December 2011), Pacific/Kiritimati
and Pacific/Enderbury (31 December 1994) and Pacific/Kwajalein (21 August 1993)
have the same five-day set of local days that no Date can carry, and the
backward walks reach every one of them.

The same normalization also dragged a shifted hour through the rest of the
walk, which addBusinessDays masked with a final setHours: the hour came back
but the minutes did not, so a walk crossing the half hour transition of
Australia/Lord_Howe moved 02:15 to 02:45.

The new src/_internals/each-local-day walks the days between two local calendar
days, given as the Date.UTC numbers of those days, with an integer counter: it
always advances, it stops after a fixed number of steps whatever the zone does,
and it never mutates a Date. Each day is yielded at noon, the one time of day
every existing local day has, so a caller reading the local year, month, day and
weekday, which is all isBusinessDay reads, always sees the day it asked for. The
five local days no zone ever had are skipped rather than yielded twice as the
day the runtime resolves them to. All three utils now drive their walk with it,
so there is one implementation to reason about instead of three.

Behaviour is unchanged for every input that did not hang or fall on a daylight
saving boundary, including the sign convention, the boundary treatment and the
1900-2099 refusal, all still pinned by the existing tests. The new time zone
suites run on Node, Bun and Deno through the inTimeZone helper of the test
runtime and are skipped where the process time zone cannot be changed.
…t at

getHolidays memoizes a year and returns copies of the Date objects it built
the first time. A Date is an instant, and the holidays of a year are local
calendar days, so once the process time zone changes the memoized answer moves
with it: under TZ=America/Sao_Paulo, a 2018 entry first computed under UTC
turns 2 November 2018 00:00 into 1 November 21:00, and Finados stops being a
holiday for the day it falls on. isBusinessDay and the walks built on it then
count that day as a business day, which is how addBusinessDays(new Date(2018,
10, 1, 9, 30), 5) answered 8 November instead of 9.

The memo now keeps the year, month and day of each holiday and builds the Date
on the way out, in the zone the caller is in. It costs nothing: the copy on the
way out already allocated one Date per holiday. The new time zone suite pins
it, and the time zone suites of the business day utils stop depending on which
test file warmed the memo first, which is why they passed on Node, where each
test file gets its own module registry, and failed on Bun, where they share
one.
…ith add/subBusinessDays

The month questions ("quinto dia útil", "último dia útil do mês") need no
utility of their own: adding n business days to the last day of the month
before gives the n-th business day of the month, and subtracting one from the
first day of the month after gives the last one. Both docs now show that
recipe under subBusinessDays, with the two ways it differs from a dedicated
function spelled out: an n beyond the business days of the month lands in the
next month, and January 1900 and December 2099 return null because the
starting day is outside the supported years. They also say this is the banking
count, not the payroll one of CLT art. 459 § 1º.

The recipe is pinned by tests: hand counted examples (Ano novo, Carnaval,
Sexta-feira Santa, Corpus Christi with and without includeOptional, a state
holiday, the month spill, the year edges and December 2011 in Pacific/Apia,
the case that used to hang) and two fast-check properties that compare it with
a brute force walk of the month through isBusinessDay, generated by the new
businessDayMonths arbitrary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
…and sub's time caveat

The day walk comment said five local days do not exist but listed four,
leaving out Pacific/Fakaofo. The recipe bullet said January 1900 and December
2099 return null, when only the n-th business day of the first and the last
business day of the second do. subBusinessDays said the time of day is always
kept, without the caveat addBusinessDays, which it delegates to, documents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH

This branch was successfully deployed

1 active deployment
Preview — 464c33d8 Deployed Sep 26, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants