diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index bb87d1a2..2201169b 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -1806,7 +1806,10 @@ isHoliday(); // false Verifica se uma data é dia útil no Brasil: não é sábado, domingo nem um feriado que `getHolidays` lista para o seu dia de calendário local. -- **Opções** (`BusinessDayOptions`, as mesmas de todos os utilitários de dias úteis): `includeOptional` (padrão `true`) também conta os feriados `"optional"`, Carnaval e Corpus Christi, como dias não úteis; `stateCode` também conta os feriados daquele estado. +- **Opções** (`BusinessDayOptions`, as mesmas de todos os utilitários de dias úteis): `includeOptional` (padrão `true`) também conta os feriados `"optional"`, Carnaval e Corpus Christi, como dias não úteis; `includeSaturday` (padrão `false`) conta o sábado como dia útil; `stateCode` também conta os feriados daquele estado. +- Com `includeSaturday` desligado, é a contagem de segunda a sexta usada por bancos e tribunais. Ligado, é a contagem trabalhista do prazo de pagamento do salário do art. 459, § 1º, da CLT, a que a fiscalização do trabalho lê pela Instrução Normativa MTP nº 2/2021, art. 14, I: "na contagem dos dias será incluído o sábado, excluindo-se o domingo e o feriado, inclusive o municipal". +- Com `includeSaturday` ligado, o domingo e os feriados continuam excluídos, então um feriado que cai em um sábado continua não sendo dia útil. +- O trecho "inclusive o municipal" dessa regra não é coberto: `getHolidays` tem apenas feriados nacionais e estaduais, então um feriado municipal é contado aqui como dia útil comum. Retire os feriados municipais por conta própria quando a contagem precisar ser exata para um município. - Retorna `false` quando `value` não é um `Date` válido ou o seu ano está fora de 1900 a 2099, ou quando `stateCode` está presente e não é string. ```javascript @@ -1815,6 +1818,9 @@ import { isBusinessDay } from '@brazilian-utils/brazilian-utils'; isBusinessDay(new Date(2024, 0, 2)); // true (terça-feira, não é feriado) isBusinessDay(new Date(2024, 0, 1)); // false (Ano novo) isBusinessDay(new Date(2024, 0, 6)); // false (sábado) +isBusinessDay(new Date(2024, 0, 6), { includeSaturday: true }); // true (contagem trabalhista) +isBusinessDay(new Date(2024, 8, 7), { includeSaturday: true }); // false (Independência, feriado em um sábado) +isBusinessDay(new Date(2024, 0, 7), { includeSaturday: true }); // false (o domingo nunca é incluído) isBusinessDay(new Date(2024, 1, 13)); // false (Carnaval, feriado facultativo, conta por padrão) isBusinessDay(new Date(2024, 1, 13), { includeOptional: false }); // true isBusinessDay(new Date(2024, 6, 9), { stateCode: 'SP' }); // false (Revolução Constitucionalista) @@ -1826,7 +1832,7 @@ isBusinessDay(new Date('not a date')); // false Soma dias úteis a uma data, pulando sábados, domingos e os feriados que `isBusinessDay` considera. Assinatura: `addBusinessDays(date, amount, options?)`, a mesma do date-fns. -- **Opções** (`BusinessDayOptions`, as mesmas de `isBusinessDay`): `includeOptional` (padrão `true`) também pula Carnaval e Corpus Christi; `stateCode` também pula os feriados daquele estado. +- **Opções** (`BusinessDayOptions`, as mesmas de `isBusinessDay`): `includeOptional` (padrão `true`) também pula Carnaval e Corpus Christi; `includeSaturday` (padrão `false`) conta o sábado como dia útil; `stateCode` também pula os feriados daquele estado. - Retorna um novo `Date`, com o horário preservado; `date` não é alterado. - `amount` igual a `0` retorna a mesma data, mesmo em fim de semana ou feriado. Um `amount` negativo anda para trás. - Retorna `null` quando `date` é inválido, `amount` não é um inteiro finito, `stateCode` não é string ou o resultado sai dos anos de 1900 a 2099. @@ -1838,6 +1844,8 @@ addBusinessDays(new Date(2024, 0, 2, 12), 1); // Date, 2024-01-03 12:00 (o dia s addBusinessDays(new Date(2024, 11, 31, 12), 1); // Date, 2025-01-02 12:00 (2025-01-01 é Ano novo, pulado) addBusinessDays(new Date(2024, 0, 5, 12), -1); // Date, 2024-01-04 12:00 (anda para trás) addBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (sem alteração, mesmo o sábado não sendo dia útil) +addBusinessDays(new Date(2024, 0, 5, 12), 1, { includeSaturday: true }); // Date, 2024-01-06 12:00 (contagem trabalhista, o sábado conta) +addBusinessDays(new Date(2024, 10, 1, 12), 1, { includeSaturday: true }); // Date, 2024-11-04 12:00 (2024-11-02 é Finados, feriado em um sábado) addBusinessDays(new Date(2024, 6, 8, 12), 1, { stateCode: 'SP' }); // Date, 2024-07-10 12:00 (2024-07-09 é a Revolução Constitucionalista em SP, pulado) addBusinessDays(new Date('not a date'), 1); // null addBusinessDays(new Date(2024, 0, 2), 1.5); // null (não é um número inteiro) @@ -1857,6 +1865,8 @@ subBusinessDays(new Date(2024, 0, 8, 12), 1); // Date, 2024-01-05 12:00 (anda pa subBusinessDays(new Date(2025, 0, 2, 12), 1); // Date, 2024-12-31 12:00 (2025-01-01 é Ano novo, pulado) subBusinessDays(new Date(2024, 0, 5, 12), -1); // Date, 2024-01-08 12:00 (anda para frente) subBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (sem alteração, mesmo o sábado não sendo dia útil) +subBusinessDays(new Date(2024, 0, 8, 12), 1, { includeSaturday: true }); // Date, 2024-01-06 12:00 (contagem trabalhista, o sábado conta) +subBusinessDays(new Date(2024, 10, 4, 12), 1, { includeSaturday: true }); // Date, 2024-11-01 12:00 (2024-11-02 é Finados, feriado em um sábado) subBusinessDays(new Date(2024, 6, 10, 12), 1, { stateCode: 'SP' }); // Date, 2024-07-08 12:00 (2024-07-09 é a Revolução Constitucionalista em SP, pulado) subBusinessDays(new Date('not a date'), 1); // null subBusinessDays(new Date(2024, 0, 2), 1.5); // null (não é um número inteiro) @@ -1866,7 +1876,7 @@ subBusinessDays(new Date(2024, 0, 2), 1.5); // null (não é um número inteiro) Conta os dias úteis entre duas datas. Assinatura: `differenceInBusinessDays(laterDate, earlierDate, options?)`, a mesma do date-fns. -- **Opções** (`BusinessDayOptions`, as mesmas de `isBusinessDay`): `includeOptional` (padrão `true`) também pula Carnaval e Corpus Christi; `stateCode` também pula os feriados daquele estado. +- **Opções** (`BusinessDayOptions`, as mesmas de `isBusinessDay`): `includeOptional` (padrão `true`) também pula Carnaval e Corpus Christi; `includeSaturday` (padrão `false`) conta o sábado como dia útil; `stateCode` também pula os feriados daquele estado. - Conta `earlierDate` quando é dia útil e cada dia útil estritamente entre as duas datas; `laterDate` nunca é contado. O horário é ignorado. - O resultado é negativo quando `laterDate` é anterior a `earlierDate`, e `0` no mesmo dia de calendário. - Retorna `null` quando uma das datas não é um `Date` válido ou está fora dos anos de 1900 a 2099, ou quando `stateCode` não é string. @@ -1878,6 +1888,9 @@ differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 1)); // 0 (01/0 differenceInBusinessDays(new Date(2024, 0, 3), new Date(2024, 0, 2)); // 1 (02/01 contado, uma terça-feira; 03/01 não) differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 3)); // -1 (a data posterior vem primeiro, então a contagem é negativa) differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 2)); // 0 (mesmo dia) +differenceInBusinessDays(new Date(2024, 0, 8), new Date(2024, 0, 1)); // 4 (contagem bancária, de 2024-01-02 a 2024-01-05) +differenceInBusinessDays(new Date(2024, 0, 8), new Date(2024, 0, 1), { includeSaturday: true }); // 5 (2024-01-06, um sábado, também conta) +differenceInBusinessDays(new Date(2024, 10, 4), new Date(2024, 10, 1), { includeSaturday: true }); // 1 (2024-11-02 é Finados, feriado em um sábado) differenceInBusinessDays(new Date(2024, 6, 10), new Date(2024, 6, 8), { stateCode: 'SP' }); // 1 (09/07/2024 é feriado estadual em SP) differenceInBusinessDays(new Date(), new Date('not a date')); // null ``` @@ -1886,7 +1899,7 @@ differenceInBusinessDays(new Date(), new Date('not a date')); // null Retorna o primeiro dia útil brasileiro estritamente depois de uma data. `getNextBusinessDay(date, options?)` é `addBusinessDays(date, 1, options)`. -- **Opções** (`BusinessDayOptions`, as mesmas de `isBusinessDay`): `includeOptional` (padrão `true`) também pula Carnaval e Corpus Christi; `stateCode` também pula os feriados daquele estado. +- **Opções** (`BusinessDayOptions`, as mesmas de `isBusinessDay`): `includeOptional` (padrão `true`) também pula Carnaval e Corpus Christi; `includeSaturday` (padrão `false`) conta o sábado como dia útil; `stateCode` também pula os feriados daquele estado. - "Próximo" significa estritamente depois, o sentido que o date-fns dá em `nextDay`: um `date` que já é dia útil nunca é retornado. Para "no dia ou depois", consulte `isBusinessDay(date)` antes e fique com `date` quando a resposta for `true`. - Retorna um novo `Date`, com o horário preservado; `date` nunca é modificado. - Retorna `null` quando `date` é inválido, `stateCode` não é string, ou o resultado sai dos anos de 1900 a 2099. @@ -1897,6 +1910,8 @@ import { getNextBusinessDay } from '@brazilian-utils/brazilian-utils'; getNextBusinessDay(new Date(2024, 0, 2, 12)); // Date, 2024-01-03 12:00 (estritamente depois, embora 02/01/2024 seja dia útil) getNextBusinessDay(new Date(2024, 0, 5, 12)); // Date, 2024-01-08 12:00 (pula o fim de semana) getNextBusinessDay(new Date(2024, 0, 6, 12)); // Date, 2024-01-08 12:00 (a partir de um sábado) +getNextBusinessDay(new Date(2024, 0, 5, 12), { includeSaturday: true }); // Date, 2024-01-06 12:00 (contagem trabalhista, o sábado conta) +getNextBusinessDay(new Date(2024, 10, 1, 12), { includeSaturday: true }); // Date, 2024-11-04 12:00 (2024-11-02 é Finados, feriado em um sábado) getNextBusinessDay(new Date(2024, 11, 31, 12)); // Date, 2025-01-02 12:00 (01/01/2025 é Ano novo, pulado) getNextBusinessDay(new Date(2024, 6, 8, 12), { stateCode: 'SP' }); // Date, 2024-07-10 12:00 (09/07/2024 é Revolução Constitucionalista em SP, pulado) getNextBusinessDay(new Date('not a date')); // null @@ -1907,12 +1922,12 @@ getNextBusinessDay(new Date(2099, 11, 31)); // null (o percurso sai dos anos sup Retorna o n-ésimo dia útil brasileiro do mês em que uma data cai. Assinatura: `getNthBusinessDay(date, n, options?)`, a ordem de argumentos de `addBusinessDays`. -- **Opções** (`BusinessDayOptions`, as mesmas de `isBusinessDay`): `includeOptional` (padrão `true`) também pula Carnaval e Corpus Christi; `stateCode` também pula os feriados daquele estado. +- **Opções** (`BusinessDayOptions`, as mesmas de `isBusinessDay`): `includeOptional` (padrão `true`) também pula Carnaval e Corpus Christi; `includeSaturday` (padrão `false`) conta o sábado como dia útil; `stateCode` também pula os feriados daquele estado. - `date` é qualquer data dentro do mês: o que importa são o ano e o mês locais, o dia do mês e o horário são ignorados. - Um `n` positivo conta a partir do primeiro dia do mês (`1` é o primeiro dia útil) e um `n` negativo conta a partir do último (`-1` é o último dia útil, `-2` o anterior a ele), como `Array#at` lê um índice. - Retorna um novo `Date` no início daquele dia local (00:00); `date` nunca é modificado. - Retorna `null` quando `date` é inválido, `n` não é um inteiro finito, é `0` ou vai além dos dias úteis que o mês tem (a resposta nunca transborda para um mês vizinho), `stateCode` não é string, ou o mês sai dos anos de 1900 a 2099. Um `options` que não é um objeto é ignorado. -- O prazo de pagamento de salário do art. 459, § 1º, da CLT ("até o quinto dia útil do mês subsequente ao vencido") é contado de outro modo pela inspeção do trabalho, que conta o sábado como dia útil e ignora os feriados municipais. O quinto dia útil retornado aqui é o bancário, que pode cair depois do trabalhista. +- O prazo de pagamento de salário do art. 459, § 1º, da CLT ("até o quinto dia útil do mês subsequente ao vencido") é a contagem trabalhista, não a bancária que esta função dá por padrão: passe `{ includeSaturday: true }`, como em `getNthBusinessDay(date, 5, { includeSaturday: true })`. Os feriados municipais continuam contando como dias úteis, e essa regra os exclui. - A contagem percorre dias do calendário local, então vale em qualquer fuso horário. Quando o dia local resultante não tem 00:00 (o horário de verão brasileiro sempre começava à meia-noite, então 6 de outubro de 1997 começa à 01:00 em São Paulo), é retornado o instante mais próximo daquele dia, o mesmo `Date` que o `startOfDay` do date-fns dá ali. - Um dia do calendário local que o fuso nunca teve, como 30 de dezembro de 2011 em `Pacific/Apia`, não é contado nem retornado. @@ -1923,6 +1938,9 @@ getNthBusinessDay(new Date(2024, 0, 15), 1); // Date, 2024-01-02 00:00 (01/01/20 getNthBusinessDay(new Date(2024, 0, 15), 5); // Date, 2024-01-08 00:00 getNthBusinessDay(new Date(2024, 1, 1), 10); // Date, 2024-02-15 00:00 (Carnaval, 13/02/2024, pulado) getNthBusinessDay(new Date(2024, 1, 1), 10, { includeOptional: false }); // Date, 2024-02-14 00:00 +getNthBusinessDay(new Date(2024, 2, 1), 5); // Date, 2024-03-07 00:00 (contagem bancária) +getNthBusinessDay(new Date(2024, 2, 1), 5, { includeSaturday: true }); // Date, 2024-03-06 00:00 (contagem trabalhista, 2024-03-02 é um sábado) +getNthBusinessDay(new Date(2024, 10, 1), 5, { includeSaturday: true }); // Date, 2024-11-07 00:00 (2024-11-02 é Finados, feriado em um sábado) getNthBusinessDay(new Date(2024, 6, 1), 7, { stateCode: 'SP' }); // Date, 2024-07-10 00:00 (09/07/2024 é Revolução Constitucionalista em SP, pulado) getNthBusinessDay(new Date(2024, 0, 15), -1); // Date, 2024-01-31 00:00 (o último dia útil) getNthBusinessDay(new Date(2024, 0, 15), -2); // Date, 2024-01-30 00:00 @@ -1946,6 +1964,8 @@ getLastBusinessDayOfMonth(new Date(2024, 2, 1)); // Date, 2024-03-28 00:00 (29/0 getLastBusinessDayOfMonth(new Date(2024, 7, 31, 18, 30)); // Date, 2024-08-30 00:00 (31/08/2024 é um sábado) getLastBusinessDayOfMonth(new Date(2018, 4, 1)); // Date, 2018-05-30 00:00 (31/05/2018 é Corpus Christi, facultativo, conta por padrão) getLastBusinessDayOfMonth(new Date(2018, 4, 1), { includeOptional: false }); // Date, 2018-05-31 00:00 +getLastBusinessDayOfMonth(new Date(2024, 7, 1), { includeSaturday: true }); // Date, 2024-08-31 00:00 (contagem trabalhista, o sábado conta) +getLastBusinessDayOfMonth(new Date(2024, 10, 1), { stateCode: 'DF', includeSaturday: true }); // Date, 2024-11-29 00:00 (o sábado 2024-11-30 é o Dia do Evangélico no DF) getLastBusinessDayOfMonth(new Date(2023, 10, 1), { stateCode: 'DF' }); // Date, 2023-11-29 00:00 (30/11/2023 é Dia do Evangélico no DF) getLastBusinessDayOfMonth(new Date('not a date')); // null ``` diff --git a/docs/utilities.md b/docs/utilities.md index 884a4ece..a4729b7e 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -1806,7 +1806,10 @@ isHoliday(); // false Check if a date is a Brazilian business day (dia útil): not a Saturday, a Sunday or a holiday `getHolidays` lists for its local calendar day. -- **Options** (`BusinessDayOptions`, shared by every business day util): `includeOptional` (default `true`) also counts the `"optional"` holidays, Carnaval and Corpus Christi, as non-business days; `stateCode` also counts that state's holidays. +- **Options** (`BusinessDayOptions`, shared by every business day util): `includeOptional` (default `true`) also counts the `"optional"` holidays, Carnaval and Corpus Christi, as non-business days; `includeSaturday` (default `false`) counts Saturday as a business day; `stateCode` also counts that state's holidays. +- `includeSaturday` off is the Monday to Friday count banks and courts use. On, it is the labour law count of the payroll deadline of CLT art. 459 § 1º, the one labour inspection reads through Instrução Normativa MTP nº 2/2021, art. 14, I: "na contagem dos dias será incluído o sábado, excluindo-se o domingo e o feriado, inclusive o municipal". +- Sunday and holidays are still excluded with `includeSaturday` on, so a holiday that falls on a Saturday is still not a business day. +- The "inclusive o municipal" part of that rule is not covered: `getHolidays` carries national and state holidays only, so a municipal holiday counts here as an ordinary business day. Remove the municipal holidays yourself when a count has to be exact for one municipality. - Returns `false` when `value` is not a valid `Date` or its year is outside 1900 to 2099, or when `stateCode` is present and not a string. ```javascript @@ -1815,6 +1818,9 @@ import { isBusinessDay } from '@brazilian-utils/brazilian-utils'; isBusinessDay(new Date(2024, 0, 2)); // true (Tuesday, not a holiday) isBusinessDay(new Date(2024, 0, 1)); // false (Ano novo) isBusinessDay(new Date(2024, 0, 6)); // false (Saturday) +isBusinessDay(new Date(2024, 0, 6), { includeSaturday: true }); // true (labour law count) +isBusinessDay(new Date(2024, 8, 7), { includeSaturday: true }); // false (Independência, a holiday on a Saturday) +isBusinessDay(new Date(2024, 0, 7), { includeSaturday: true }); // false (Sunday is never included) isBusinessDay(new Date(2024, 1, 13)); // false (Carnaval, optional holiday, counts by default) isBusinessDay(new Date(2024, 1, 13), { includeOptional: false }); // true isBusinessDay(new Date(2024, 6, 9), { stateCode: 'SP' }); // false (Revolução Constitucionalista) @@ -1826,7 +1832,7 @@ isBusinessDay(new Date('not a date')); // false Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and the holidays `isBusinessDay` skips. Signature: `addBusinessDays(date, amount, options?)`, the same as date-fns. -- **Options** (`BusinessDayOptions`, shared with `isBusinessDay`): `includeOptional` (default `true`) also skips Carnaval and Corpus Christi; `stateCode` also skips that state's holidays. +- **Options** (`BusinessDayOptions`, shared with `isBusinessDay`): `includeOptional` (default `true`) also skips Carnaval and Corpus Christi; `includeSaturday` (default `false`) counts Saturday as a business day; `stateCode` also skips that state's holidays. - Returns a new `Date`, time of day preserved; `date` is never mutated. - An `amount` of `0` returns the same date, even on a weekend or holiday. A negative `amount` walks backwards. - Returns `null` when `date` is invalid, `amount` is not a finite integer, `stateCode` is not a string, or the result leaves the years 1900 to 2099. @@ -1838,6 +1844,8 @@ addBusinessDays(new Date(2024, 0, 2, 12), 1); // Date, 2024-01-03 12:00 (next da addBusinessDays(new Date(2024, 11, 31, 12), 1); // Date, 2025-01-02 12:00 (2025-01-01 is Ano novo, skipped) addBusinessDays(new Date(2024, 0, 5, 12), -1); // Date, 2024-01-04 12:00 (walks backwards) addBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (unchanged, even though Saturday is not a business day) +addBusinessDays(new Date(2024, 0, 5, 12), 1, { includeSaturday: true }); // Date, 2024-01-06 12:00 (labour law count, Saturday counts) +addBusinessDays(new Date(2024, 10, 1, 12), 1, { includeSaturday: true }); // Date, 2024-11-04 12:00 (2024-11-02 is Finados, a holiday on a Saturday) addBusinessDays(new Date(2024, 6, 8, 12), 1, { stateCode: 'SP' }); // Date, 2024-07-10 12:00 (2024-07-09 is Revolução Constitucionalista in SP, skipped) addBusinessDays(new Date('not a date'), 1); // null addBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) @@ -1857,6 +1865,8 @@ subBusinessDays(new Date(2024, 0, 8, 12), 1); // Date, 2024-01-05 12:00 (walks b subBusinessDays(new Date(2025, 0, 2, 12), 1); // Date, 2024-12-31 12:00 (2025-01-01 is Ano novo, skipped) subBusinessDays(new Date(2024, 0, 5, 12), -1); // Date, 2024-01-08 12:00 (walks forwards) subBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (unchanged, even though Saturday is not a business day) +subBusinessDays(new Date(2024, 0, 8, 12), 1, { includeSaturday: true }); // Date, 2024-01-06 12:00 (labour law count, Saturday counts) +subBusinessDays(new Date(2024, 10, 4, 12), 1, { includeSaturday: true }); // Date, 2024-11-01 12:00 (2024-11-02 is Finados, a holiday on a Saturday) subBusinessDays(new Date(2024, 6, 10, 12), 1, { stateCode: 'SP' }); // Date, 2024-07-08 12:00 (2024-07-09 is Revolução Constitucionalista in SP, skipped) subBusinessDays(new Date('not a date'), 1); // null subBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) @@ -1866,7 +1876,7 @@ subBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) Count the Brazilian business days (dias úteis) between two dates. Signature: `differenceInBusinessDays(laterDate, earlierDate, options?)`, the same as date-fns. -- **Options** (`BusinessDayOptions`, shared with `isBusinessDay`): `includeOptional` (default `true`) also skips Carnaval and Corpus Christi; `stateCode` also skips that state's holidays. +- **Options** (`BusinessDayOptions`, shared with `isBusinessDay`): `includeOptional` (default `true`) also skips Carnaval and Corpus Christi; `includeSaturday` (default `false`) counts Saturday as a business day; `stateCode` also skips that state's holidays. - Counts `earlierDate` when it is a business day and every business day strictly between the two dates; `laterDate` is never counted. The time of day is ignored. - The result is negative when `laterDate` is before `earlierDate`, and `0` on the same calendar day. - Returns `null` when either date is not a valid `Date` or is outside the years 1900 to 2099, or `stateCode` is not a string. @@ -1878,6 +1888,9 @@ differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 1)); // 0 (Jan differenceInBusinessDays(new Date(2024, 0, 3), new Date(2024, 0, 2)); // 1 (Jan 2 counted, a Tuesday; Jan 3 is not) differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 3)); // -1 (the later date comes first, so the count is negative) differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 2)); // 0 (same day) +differenceInBusinessDays(new Date(2024, 0, 8), new Date(2024, 0, 1)); // 4 (banking count, 2024-01-02 to 2024-01-05) +differenceInBusinessDays(new Date(2024, 0, 8), new Date(2024, 0, 1), { includeSaturday: true }); // 5 (2024-01-06, a Saturday, also counts) +differenceInBusinessDays(new Date(2024, 10, 4), new Date(2024, 10, 1), { includeSaturday: true }); // 1 (2024-11-02 is Finados, a holiday on a Saturday) differenceInBusinessDays(new Date(2024, 6, 10), new Date(2024, 6, 8), { stateCode: 'SP' }); // 1 (2024-07-09 is a state holiday in SP) differenceInBusinessDays(new Date(), new Date('not a date')); // null ``` @@ -1886,7 +1899,7 @@ differenceInBusinessDays(new Date(), new Date('not a date')); // null Get the first Brazilian business day (dia útil) strictly after a date. `getNextBusinessDay(date, options?)` is `addBusinessDays(date, 1, options)`. -- **Options** (`BusinessDayOptions`, shared with `isBusinessDay`): `includeOptional` (default `true`) also skips Carnaval and Corpus Christi; `stateCode` also skips that state's holidays. +- **Options** (`BusinessDayOptions`, shared with `isBusinessDay`): `includeOptional` (default `true`) also skips Carnaval and Corpus Christi; `includeSaturday` (default `false`) counts Saturday as a business day; `stateCode` also skips that state's holidays. - "Next" means strictly after, the meaning date-fns gives it in `nextDay`: a `date` that is itself a business day is never returned. For "on or after", check `isBusinessDay(date)` first and keep `date` when it answers `true`. - Returns a new `Date`, time of day preserved; `date` is never mutated. - Returns `null` when `date` is invalid, `stateCode` is not a string, or the result leaves the years 1900 to 2099. @@ -1897,6 +1910,8 @@ import { getNextBusinessDay } from '@brazilian-utils/brazilian-utils'; getNextBusinessDay(new Date(2024, 0, 2, 12)); // Date, 2024-01-03 12:00 (strictly after, even though 2024-01-02 is a business day) getNextBusinessDay(new Date(2024, 0, 5, 12)); // Date, 2024-01-08 12:00 (skips the weekend) getNextBusinessDay(new Date(2024, 0, 6, 12)); // Date, 2024-01-08 12:00 (from a Saturday) +getNextBusinessDay(new Date(2024, 0, 5, 12), { includeSaturday: true }); // Date, 2024-01-06 12:00 (labour law count, Saturday counts) +getNextBusinessDay(new Date(2024, 10, 1, 12), { includeSaturday: true }); // Date, 2024-11-04 12:00 (2024-11-02 is Finados, a holiday on a Saturday) getNextBusinessDay(new Date(2024, 11, 31, 12)); // Date, 2025-01-02 12:00 (2025-01-01 is Ano novo, skipped) getNextBusinessDay(new Date(2024, 6, 8, 12), { stateCode: 'SP' }); // Date, 2024-07-10 12:00 (2024-07-09 is Revolução Constitucionalista in SP, skipped) getNextBusinessDay(new Date('not a date')); // null @@ -1907,12 +1922,12 @@ getNextBusinessDay(new Date(2099, 11, 31)); // null (the walk leaves the support Get the n-th Brazilian business day (dia útil) of the month a date falls in. Signature: `getNthBusinessDay(date, n, options?)`, the argument order of `addBusinessDays`. -- **Options** (`BusinessDayOptions`, shared with `isBusinessDay`): `includeOptional` (default `true`) also skips Carnaval and Corpus Christi; `stateCode` also skips that state's holidays. +- **Options** (`BusinessDayOptions`, shared with `isBusinessDay`): `includeOptional` (default `true`) also skips Carnaval and Corpus Christi; `includeSaturday` (default `false`) counts Saturday as a business day; `stateCode` also skips that state's holidays. - `date` is any date inside the month: its local year and month are what matter, the day of the month and the time of day are ignored. - A positive `n` counts from the first day of the month (`1` is the first business day) and a negative `n` counts from the last one (`-1` is the last business day, `-2` the one before it), the way `Array#at` reads an index. - Returns a new `Date` at the start of that local day (00:00); `date` is never mutated. - Returns `null` when `date` is invalid, `n` is not a finite integer, is `0` or goes beyond the business days the month has (the answer never spills into a neighbouring month), `stateCode` is not a string, or the month leaves the years 1900 to 2099. An `options` that is not an object is ignored. -- The payroll deadline of CLT art. 459 § 1º ("até o quinto dia útil do mês subsequente ao vencido") is counted differently by labour inspection, which counts Saturday as a business day and ignores municipal holidays. The fifth business day returned here is the banking one, which can fall after the labour one. +- The payroll deadline of CLT art. 459 § 1º ("até o quinto dia útil do mês subsequente ao vencido") is the labour count, not the banking one this gives by default: pass `{ includeSaturday: true }`, as in `getNthBusinessDay(date, 5, { includeSaturday: true })`. Municipal holidays are still counted as business days, which that rule excludes. - The walk counts local calendar days, so it holds in every time zone. Where the resulting local day has no 00:00 (Brazilian summer time always started at midnight, so 6 October 1997 begins at 01:00 in São Paulo), the nearest instant of that day is returned, the same `Date` date-fns' `startOfDay` gives there. - A local calendar day a zone never had, such as 30 December 2011 in `Pacific/Apia`, is neither counted nor returned. @@ -1923,6 +1938,9 @@ getNthBusinessDay(new Date(2024, 0, 15), 1); // Date, 2024-01-02 00:00 (2024-01- getNthBusinessDay(new Date(2024, 0, 15), 5); // Date, 2024-01-08 00:00 getNthBusinessDay(new Date(2024, 1, 1), 10); // Date, 2024-02-15 00:00 (Carnaval, 2024-02-13, skipped) getNthBusinessDay(new Date(2024, 1, 1), 10, { includeOptional: false }); // Date, 2024-02-14 00:00 +getNthBusinessDay(new Date(2024, 2, 1), 5); // Date, 2024-03-07 00:00 (banking count) +getNthBusinessDay(new Date(2024, 2, 1), 5, { includeSaturday: true }); // Date, 2024-03-06 00:00 (labour law count, 2024-03-02 is a Saturday) +getNthBusinessDay(new Date(2024, 10, 1), 5, { includeSaturday: true }); // Date, 2024-11-07 00:00 (2024-11-02 is Finados, a holiday on a Saturday) getNthBusinessDay(new Date(2024, 6, 1), 7, { stateCode: 'SP' }); // Date, 2024-07-10 00:00 (2024-07-09 is Revolução Constitucionalista in SP, skipped) getNthBusinessDay(new Date(2024, 0, 15), -1); // Date, 2024-01-31 00:00 (the last business day) getNthBusinessDay(new Date(2024, 0, 15), -2); // Date, 2024-01-30 00:00 @@ -1946,6 +1964,8 @@ getLastBusinessDayOfMonth(new Date(2024, 2, 1)); // Date, 2024-03-28 00:00 (2024 getLastBusinessDayOfMonth(new Date(2024, 7, 31, 18, 30)); // Date, 2024-08-30 00:00 (2024-08-31 is a Saturday) getLastBusinessDayOfMonth(new Date(2018, 4, 1)); // Date, 2018-05-30 00:00 (2018-05-31 is Corpus Christi, optional, counts by default) getLastBusinessDayOfMonth(new Date(2018, 4, 1), { includeOptional: false }); // Date, 2018-05-31 00:00 +getLastBusinessDayOfMonth(new Date(2024, 7, 1), { includeSaturday: true }); // Date, 2024-08-31 00:00 (labour law count, Saturday counts) +getLastBusinessDayOfMonth(new Date(2024, 10, 1), { stateCode: 'DF', includeSaturday: true }); // Date, 2024-11-29 00:00 (Saturday 2024-11-30 is Dia do Evangélico in DF) getLastBusinessDayOfMonth(new Date(2023, 10, 1), { stateCode: 'DF' }); // Date, 2023-11-29 00:00 (2023-11-30 is Dia do Evangélico in DF) getLastBusinessDayOfMonth(new Date('not a date')); // null ``` diff --git a/reports/api/brazilian-utils.api.md b/reports/api/brazilian-utils.api.md index 78c54274..42dbb29f 100644 --- a/reports/api/brazilian-utils.api.md +++ b/reports/api/brazilian-utils.api.md @@ -48,6 +48,7 @@ export type BoletoInfo = { export type BusinessDayOptions = { stateCode?: StateCode; includeOptional?: boolean; + includeSaturday?: boolean; }; // @public diff --git a/src/_internals/test/arbitraries.ts b/src/_internals/test/arbitraries.ts index b5c9a94a..bcefa0f7 100644 --- a/src/_internals/test/arbitraries.ts +++ b/src/_internals/test/arbitraries.ts @@ -195,11 +195,16 @@ export const anyBusinessDayAmount: fc.Arbitrary = fc.oneof( const anyStateCode = fc.oneof(fc.constantFrom(...PROTOTYPE_KEYS, "SP", "xx"), fc.anything()); const anyIncludeOptional = fc.oneof(fc.boolean(), fc.anything()); +const anyIncludeSaturday = fc.oneof(fc.boolean(), fc.anything()); /** Business day options, or anything at all, prototype chain keys as the state code included. */ export const anyBusinessDayOptions: fc.Arbitrary = fc.oneof( fc.anything(), - fc.record({ stateCode: anyStateCode, includeOptional: anyIncludeOptional }), + fc.record({ + stateCode: anyStateCode, + includeOptional: anyIncludeOptional, + includeSaturday: anyIncludeSaturday, + }), ); /** An amount with at most two decimals, the precision currency formatting round-trips. */ diff --git a/src/add-business-days/add-business-days.test.ts b/src/add-business-days/add-business-days.test.ts index 691c2250..09628839 100644 --- a/src/add-business-days/add-business-days.test.ts +++ b/src/add-business-days/add-business-days.test.ts @@ -97,6 +97,51 @@ describe("addBusinessDays", () => { }); }); + describe("includeSaturday", () => { + it("should land on Saturday when it counts (Fri 2024-01-05 + 1 -> Sat 2024-01-06)", () => { + const result = addBusinessDays(new Date(2024, 0, 5, 12), 1, { includeSaturday: true }); + + expect(result).toEqual(new Date(2024, 0, 6, 12)); + }); + + it("should keep skipping the whole weekend without the option (Fri 2024-01-05 + 1 -> Mon 2024-01-08)", () => { + const result = addBusinessDays(new Date(2024, 0, 5, 12), 1); + + expect(result).toEqual(new Date(2024, 0, 8, 12)); + }); + + it("should never land on a Sunday (Sat 2024-01-06 + 1 -> Mon 2024-01-08)", () => { + const result = addBusinessDays(new Date(2024, 0, 6, 12), 1, { includeSaturday: true }); + + expect(result).toEqual(new Date(2024, 0, 8, 12)); + }); + + it("should still skip a holiday that falls on a Saturday (Fri 2024-11-01 + 1 -> Mon 2024-11-04, Finados)", () => { + const result = addBusinessDays(new Date(2024, 10, 1, 12), 1, { includeSaturday: true }); + + expect(result).toEqual(new Date(2024, 10, 4, 12)); + }); + + it("should count a full week as six business days (Mon 2024-01-08 + 6 -> Mon 2024-01-15, with Sat 2024-01-13)", () => { + expect(addBusinessDays(new Date(2024, 0, 8, 12), 6, { includeSaturday: true })).toEqual( + new Date(2024, 0, 15, 12), + ); + expect(addBusinessDays(new Date(2024, 0, 8, 12), 6)).toEqual(new Date(2024, 0, 16, 12)); + }); + + it("should walk backwards over Saturday too (Mon 2024-01-08 - 1 -> Sat 2024-01-06)", () => { + const result = addBusinessDays(new Date(2024, 0, 8, 12), -1, { includeSaturday: true }); + + expect(result).toEqual(new Date(2024, 0, 6, 12)); + }); + + it("should give the same result as no options when includeSaturday is false", () => { + expect(addBusinessDays(new Date(2024, 0, 5, 12), 1, { includeSaturday: false })).toEqual( + new Date(2024, 0, 8, 12), + ); + }); + }); + describe("negative amounts", () => { it("should walk backwards, skipping weekends (Fri 2024-01-05 - 1 -> Thu 2024-01-04)", () => { const result = addBusinessDays(new Date(2024, 0, 5, 12), -1); diff --git a/src/add-business-days/add-business-days.ts b/src/add-business-days/add-business-days.ts index 5939161c..57f9c3a1 100644 --- a/src/add-business-days/add-business-days.ts +++ b/src/add-business-days/add-business-days.ts @@ -10,7 +10,8 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * Adds a number of Brazilian business days (dias úteis) to a date. * * A business day is a day for which `isBusinessDay` returns `true` (not a Saturday, a - * Sunday, or a Brazilian holiday), evaluated with the same `options`. The function walks one + * Sunday, or a Brazilian holiday; `options.includeSaturday` keeps Saturday), evaluated with + * the same `options`. The function walks one * calendar day at a time, in the direction of `amount`, counting only business days, so it is * exact regardless of the arrangement of holidays around `date` (cheap in practice: * `getHolidays` is memoized per year). @@ -28,6 +29,12 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * not have, such as `00:30` on a day whose clocks jump from `00:00` to `01:00`: the result is * then the nearest instant of that day, `01:30`. * + * `options.includeSaturday` defaults to `false`, the Monday to Friday banking count. Pass `true` + * for the labour law count of Instrução Normativa MTP nº 2/2021, art. 14, I, which includes + * Saturday and still excludes Sunday and holidays, so a holiday that falls on a Saturday is never + * counted. See `isBusinessDay` for the law behind it and for what it does not cover: municipal + * holidays, which `getHolidays` does not carry. + * * If `options.stateCode` is provided but is not a valid/known state code, it is ignored and * only national holidays are considered (same behavior as `getHolidays`/`isBusinessDay`), so a * prototype-chain key such as `"__proto__"` is an unknown state code like any other. An @@ -38,9 +45,10 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * * @param {Date} date - The date to count from. Never mutated: a new `Date` is returned. * @param {number} amount - The number of business days to add; a negative value walks backwards. - * @param {BusinessDayOptions} [options] - Which holidays count as non-business days. + * @param {BusinessDayOptions} [options] - Which days count as business days. * @param {StateCode} [options.stateCode] - Brazilian state code whose state holidays are also considered. * @param {boolean} [options.includeOptional] - Whether optional holidays count as non-business days (default: `true`). + * @param {boolean} [options.includeSaturday] - Whether Saturday counts as a business day (default: `false`). * @returns {Date | null} A new `Date`, `amount` business days after `date`. `null` on bad * input: a `date` that is not a valid `Date` or is outside 1900-2099, an `amount` that is not a * finite integer, a `stateCode` that is not a string, or a walk that leaves the supported years. @@ -51,6 +59,8 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * addBusinessDays(new Date(2024, 11, 31, 12), 1); // Thu 2025-01-02, 12:00 (Jan 1 is Ano novo, skipped) * addBusinessDays(new Date(2024, 0, 5, 12), -1); // Thu 2024-01-04, 12:00 (walks backwards) * addBusinessDays(new Date(2024, 0, 6, 12), 0); // Sat 2024-01-06, 12:00 (unchanged, even though Saturday is not a business day) + * addBusinessDays(new Date(2024, 0, 5, 12), 1, { includeSaturday: true }); // Sat 2024-01-06, 12:00 (labour count) + * addBusinessDays(new Date(2024, 10, 1, 12), 1, { includeSaturday: true }); // Mon 2024-11-04, 12:00 (Nov 2 is Finados, a holiday on a Saturday) * addBusinessDays(new Date(2024, 6, 8, 12), 1, { stateCode: "SP" }); // Wed 2024-07-10, 12:00 (Jul 9 is a state holiday in SP) * addBusinessDays(new Date("not a date"), 1); // null * addBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) diff --git a/src/difference-in-business-days/difference-in-business-days.test.ts b/src/difference-in-business-days/difference-in-business-days.test.ts index aef04799..c5cd34fe 100644 --- a/src/difference-in-business-days/difference-in-business-days.test.ts +++ b/src/difference-in-business-days/difference-in-business-days.test.ts @@ -100,6 +100,51 @@ describe("differenceInBusinessDays", () => { }); }); + describe("includeSaturday", () => { + it("should count the Saturday of the first week of 2024 (Mon 2024-01-01 to Mon 2024-01-08: 4 without the option, 5 with it)", () => { + expect(differenceInBusinessDays(new Date(2024, 0, 8), new Date(2024, 0, 1))).toBe(4); + expect( + differenceInBusinessDays(new Date(2024, 0, 8), new Date(2024, 0, 1), { + includeSaturday: true, + }), + ).toBe(5); + }); + + it("should skip Finados on Saturday 2024-11-02 and count the plain Saturday 2024-11-09 (Nov 1 to Nov 11: 6 without the option, 7 with it)", () => { + expect(differenceInBusinessDays(new Date(2024, 10, 11), new Date(2024, 10, 1))).toBe(6); + expect( + differenceInBusinessDays(new Date(2024, 10, 11), new Date(2024, 10, 1), { + includeSaturday: true, + }), + ).toBe(7); + }); + + it("should count only Friday 2024-11-01 over a Saturday holiday and a Sunday (Nov 1 to Nov 4)", () => { + const result = differenceInBusinessDays(new Date(2024, 10, 4), new Date(2024, 10, 1), { + includeSaturday: true, + }); + + expect(result).toBe(1); + }); + + it("should keep the sign convention when the later date comes first (Mon 2024-01-08 down to Tue 2024-01-02: -5 without the option, -6 with it)", () => { + expect(differenceInBusinessDays(new Date(2024, 0, 1), new Date(2024, 0, 8))).toBe(-5); + expect( + differenceInBusinessDays(new Date(2024, 0, 1), new Date(2024, 0, 8), { + includeSaturday: true, + }), + ).toBe(-6); + }); + + it("should give the same count as no options when includeSaturday is false", () => { + const result = differenceInBusinessDays(new Date(2024, 0, 8), new Date(2024, 0, 1), { + includeSaturday: false, + }); + + expect(result).toBe(4); + }); + }); + describe("invalid input", () => { it("should return null when called without arguments", () => { // @ts-expect-error: intentionally invalid input @@ -168,12 +213,28 @@ describe("differenceInBusinessDays", () => { it("should count the 20 business days December 2011 has there, the missing 30th excluded", () => { expect(differenceInBusinessDays(new Date(2011, 11, 1), new Date(2011, 11, 31))).toBe(-20); }); + + it("should add the five Saturdays of 2 to 31 December 2011 with includeSaturday, still without the 30th (-25)", () => { + const result = differenceInBusinessDays(new Date(2011, 11, 1), new Date(2011, 11, 31), { + includeSaturday: true, + }); + + expect(result).toBe(-25); + }); }); inTimeZone("UTC", () => { it("should count 21 for the same December, where the 30th is an ordinary Friday", () => { expect(differenceInBusinessDays(new Date(2011, 11, 1), new Date(2011, 11, 31))).toBe(-21); }); + + it("should count 26 for the same December with includeSaturday", () => { + const result = differenceInBusinessDays(new Date(2011, 11, 1), new Date(2011, 11, 31), { + includeSaturday: true, + }); + + expect(result).toBe(-26); + }); }); inTimeZone("America/Sao_Paulo", () => { diff --git a/src/difference-in-business-days/difference-in-business-days.ts b/src/difference-in-business-days/difference-in-business-days.ts index f0f412b9..383c3034 100644 --- a/src/difference-in-business-days/difference-in-business-days.ts +++ b/src/difference-in-business-days/difference-in-business-days.ts @@ -23,7 +23,13 @@ const toLocalDayTimestamp = (date: Date): number => * zero, never `-0`). * * A business day is a day for which `isBusinessDay` returns `true` (not a Saturday, a Sunday, - * or a Brazilian holiday), evaluated with the same `options`. + * or a Brazilian holiday; `options.includeSaturday` keeps Saturday), evaluated with the same `options`. + * + * `options.includeSaturday` defaults to `false`, the Monday to Friday banking count. Pass `true` + * for the labour law count of Instrução Normativa MTP nº 2/2021, art. 14, I, which includes + * Saturday and still excludes Sunday and holidays, so a holiday that falls on a Saturday is never + * counted. See `isBusinessDay` for the law behind it and for what it does not cover: municipal + * holidays, which `getHolidays` does not carry. * * If `options.stateCode` is provided but is not a valid/known state code, it is ignored and only * national holidays are considered (same behavior as `getHolidays`/`isBusinessDay`), so a @@ -35,9 +41,10 @@ const toLocalDayTimestamp = (date: Date): number => * * @param {Date} laterDate - The date to count to. Never counted itself, regardless of whether it is a business day. * @param {Date} earlierDate - The date to count from. Counted as a business day when it is one; never mutated. - * @param {BusinessDayOptions} [options] - Which holidays count as non-business days. + * @param {BusinessDayOptions} [options] - Which days count as business days. * @param {StateCode} [options.stateCode] - Brazilian state code whose state holidays are also considered. * @param {boolean} [options.includeOptional] - Whether optional holidays count as non-business days (default: `true`). + * @param {boolean} [options.includeSaturday] - Whether Saturday counts as a business day (default: `false`). * @returns {number | null} The number of business days between the two dates, or `null` on bad * input: a `laterDate`/`earlierDate` that is not a valid `Date` or is outside 1900-2099, or a * `stateCode` that is not a string. @@ -48,6 +55,9 @@ const toLocalDayTimestamp = (date: Date): number => * differenceInBusinessDays(new Date(2024, 0, 3), new Date(2024, 0, 2)); // 1 (Jan 2 counted, a Tuesday; Jan 3 is not) * differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 3)); // -1 (the later date comes first, so the count is negative) * differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 2)); // 0 (same day) + * differenceInBusinessDays(new Date(2024, 0, 8), new Date(2024, 0, 1)); // 4 (Jan 2 to Jan 5, banking count) + * differenceInBusinessDays(new Date(2024, 0, 8), new Date(2024, 0, 1), { includeSaturday: true }); // 5 (Jan 6, a Saturday, also counts) + * differenceInBusinessDays(new Date(2024, 10, 4), new Date(2024, 10, 1), { includeSaturday: true }); // 1 (Nov 2 is Finados, a holiday on a Saturday) * differenceInBusinessDays(new Date(2024, 6, 10), new Date(2024, 6, 8), { stateCode: "SP" }); // 1 (Jul 9 is a state holiday in SP) * differenceInBusinessDays(new Date(), new Date("not a date")); // null * differenceInBusinessDays(new Date(2100, 0, 5), new Date(2100, 0, 4)); // null (outside the supported years) diff --git a/src/get-last-business-day-of-month/get-last-business-day-of-month.test.ts b/src/get-last-business-day-of-month/get-last-business-day-of-month.test.ts index 9841387a..aafc8652 100644 --- a/src/get-last-business-day-of-month/get-last-business-day-of-month.test.ts +++ b/src/get-last-business-day-of-month/get-last-business-day-of-month.test.ts @@ -71,6 +71,29 @@ describe("getLastBusinessDayOfMonth", () => { }); }); + describe("includeSaturday", () => { + it("should return the Saturday August 2024 ends on when it counts (Sat 2024-08-31, Fri 2024-08-30 without the option)", () => { + expect(getLastBusinessDayOfMonth(new Date(2024, 7, 1), { includeSaturday: true })).toEqual( + new Date(2024, 7, 31), + ); + expect(getLastBusinessDayOfMonth(new Date(2024, 7, 1), { includeSaturday: false })).toEqual( + new Date(2024, 7, 30), + ); + }); + + it("should return Sat 2024-11-30 nationally, and Fri 2024-11-29 in the DF, where that Saturday is Dia do Evangélico", () => { + expect(getLastBusinessDayOfMonth(new Date(2024, 10, 1), { includeSaturday: true })).toEqual( + new Date(2024, 10, 30), + ); + expect( + getLastBusinessDayOfMonth(new Date(2024, 10, 1), { + stateCode: "DF", + includeSaturday: true, + }), + ).toEqual(new Date(2024, 10, 29)); + }); + }); + describe("invalid input", () => { it("should return null for a date that is not a valid Date", () => { // @ts-expect-error: intentionally invalid input diff --git a/src/get-last-business-day-of-month/get-last-business-day-of-month.ts b/src/get-last-business-day-of-month/get-last-business-day-of-month.ts index cbcc35e6..05adca8a 100644 --- a/src/get-last-business-day-of-month/get-last-business-day-of-month.ts +++ b/src/get-last-business-day-of-month/get-last-business-day-of-month.ts @@ -8,7 +8,8 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * * It is `getNthBusinessDay(date, -1, options)`, which it delegates to, down to the last detail. A * business day is a day for which `isBusinessDay` returns `true` (not a Saturday, a Sunday, or a - * Brazilian holiday), evaluated with the same `options`, and the month is the one of `date`'s + * Brazilian holiday; `options.includeSaturday` keeps Saturday), evaluated with the same + * `options`, and the month is the one of `date`'s * **local calendar day**; the day of the month and the time of day of `date` are ignored. * * The name and the result follow date-fns' `lastDayOfMonth`: a new `Date` at the start of that @@ -16,6 +17,12 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * over that local midnight, the nearest representable instant of the day is returned instead, * exactly as in `getNthBusinessDay`. * + * `options.includeSaturday` defaults to `false`, the Monday to Friday banking count. Pass `true` + * for the labour law count of Instrução Normativa MTP nº 2/2021, art. 14, I, which includes + * Saturday and still excludes Sunday and holidays, so a holiday that falls on a Saturday is never + * counted. See `isBusinessDay` for the law behind it and for what it does not cover: municipal + * holidays, which `getHolidays` does not carry. + * * If `options.stateCode` is provided but is not a valid/known state code, it is ignored and only * national holidays are considered (same behavior as `getHolidays`/`isBusinessDay`), so a * prototype-chain key such as `"__proto__"` is an unknown state code like any other. An `options` @@ -25,9 +32,10 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * outside it returns `null`. * * @param {Date} date - Any date inside the month to look at. Never mutated: a new `Date` is returned. - * @param {BusinessDayOptions} [options] - Which holidays count as non-business days. + * @param {BusinessDayOptions} [options] - Which days count as business days. * @param {StateCode} [options.stateCode] - Brazilian state code whose state holidays are also considered. * @param {boolean} [options.includeOptional] - Whether optional holidays count as non-business days (default: `true`). + * @param {boolean} [options.includeSaturday] - Whether Saturday counts as a business day (default: `false`). * @returns {Date | null} A new `Date` at 00:00 local time of the last business day of the month. * `null` on bad input: a `date` that is not a valid `Date` or is outside 1900-2099, or a * `stateCode` that is not a string. @@ -39,6 +47,8 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * getLastBusinessDayOfMonth(new Date(2024, 7, 31, 18, 30)); // Fri 2024-08-30, 00:00 (Aug 31 is a Saturday) * getLastBusinessDayOfMonth(new Date(2018, 4, 1)); // Wed 2018-05-30, 00:00 (May 31 is Corpus Christi, optional, counted by default) * getLastBusinessDayOfMonth(new Date(2018, 4, 1), { includeOptional: false }); // Thu 2018-05-31, 00:00 + * getLastBusinessDayOfMonth(new Date(2024, 7, 1), { includeSaturday: true }); // Sat 2024-08-31, 00:00 (labour count) + * getLastBusinessDayOfMonth(new Date(2024, 10, 1), { stateCode: "DF", includeSaturday: true }); // Fri 2024-11-29, 00:00 (Sat Nov 30 is Dia do Evangélico in DF) * getLastBusinessDayOfMonth(new Date(2023, 10, 1), { stateCode: "DF" }); // Wed 2023-11-29, 00:00 (Nov 30 is Dia do Evangélico in DF) * getLastBusinessDayOfMonth(new Date("not a date")); // null * getLastBusinessDayOfMonth(new Date(2100, 0, 15)); // null (outside the supported years) diff --git a/src/get-next-business-day/get-next-business-day.test.ts b/src/get-next-business-day/get-next-business-day.test.ts index d953a0da..a4bcd576 100644 --- a/src/get-next-business-day/get-next-business-day.test.ts +++ b/src/get-next-business-day/get-next-business-day.test.ts @@ -75,6 +75,32 @@ describe("getNextBusinessDay", () => { }); }); + describe("includeSaturday", () => { + it("should return the Saturday when it counts (Fri 2024-01-05 -> Sat 2024-01-06)", () => { + const result = getNextBusinessDay(new Date(2024, 0, 5, 12), { includeSaturday: true }); + + expect(result).toEqual(new Date(2024, 0, 6, 12)); + }); + + it("should return the Monday without the option (Fri 2024-01-05 -> Mon 2024-01-08)", () => { + expect(getNextBusinessDay(new Date(2024, 0, 5, 12), { includeSaturday: false })).toEqual( + new Date(2024, 0, 8, 12), + ); + }); + + it("should never return a Sunday (Sat 2024-01-06 -> Mon 2024-01-08)", () => { + const result = getNextBusinessDay(new Date(2024, 0, 6, 12), { includeSaturday: true }); + + expect(result).toEqual(new Date(2024, 0, 8, 12)); + }); + + it("should skip a holiday that falls on a Saturday (Fri 2024-11-01 -> Mon 2024-11-04, Finados)", () => { + const result = getNextBusinessDay(new Date(2024, 10, 1, 12), { includeSaturday: true }); + + expect(result).toEqual(new Date(2024, 10, 4, 12)); + }); + }); + describe("invalid input", () => { it("should return null for a date that is not a valid Date", () => { // @ts-expect-error: intentionally invalid input diff --git a/src/get-next-business-day/get-next-business-day.ts b/src/get-next-business-day/get-next-business-day.ts index 381ed32d..ed64ade0 100644 --- a/src/get-next-business-day/get-next-business-day.ts +++ b/src/get-next-business-day/get-next-business-day.ts @@ -8,7 +8,7 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * * It is `addBusinessDays(date, 1, options)`, which it delegates to, down to the last detail. A * business day is a day for which `isBusinessDay` returns `true` (not a Saturday, a Sunday, or a - * Brazilian holiday), evaluated with the same `options`. + * Brazilian holiday; `options.includeSaturday` keeps Saturday), evaluated with the same `options`. * * "Next" is **strictly after**, the meaning date-fns gives it in `nextDay`, `nextMonday` and * their siblings, verified against its source: `nextDay(date, day)` on a date that already is @@ -19,6 +19,12 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * The time-of-day (hours, minutes, seconds, milliseconds) of `date` is preserved in the result, * as date-fns' `nextDay` and `addBusinessDays` do, and `date` itself is never mutated. * + * `options.includeSaturday` defaults to `false`, the Monday to Friday banking count. Pass `true` + * for the labour law count of Instrução Normativa MTP nº 2/2021, art. 14, I, which includes + * Saturday and still excludes Sunday and holidays, so a holiday that falls on a Saturday is never + * counted. See `isBusinessDay` for the law behind it and for what it does not cover: municipal + * holidays, which `getHolidays` does not carry. + * * If `options.stateCode` is provided but is not a valid/known state code, it is ignored and only * national holidays are considered (same behavior as `getHolidays`/`isBusinessDay`), so a * prototype-chain key such as `"__proto__"` is an unknown state code like any other. An `options` @@ -28,9 +34,10 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * outside it, or a walk that leaves it, returns `null`. * * @param {Date} date - The date to look after. Never mutated: a new `Date` is returned. - * @param {BusinessDayOptions} [options] - Which holidays count as non-business days. + * @param {BusinessDayOptions} [options] - Which days count as business days. * @param {StateCode} [options.stateCode] - Brazilian state code whose state holidays are also considered. * @param {boolean} [options.includeOptional] - Whether optional holidays count as non-business days (default: `true`). + * @param {boolean} [options.includeSaturday] - Whether Saturday counts as a business day (default: `false`). * @returns {Date | null} A new `Date`, the first business day after `date`. `null` on bad input: * a `date` that is not a valid `Date` or is outside 1900-2099, a `stateCode` that is not a * string, or a walk that leaves the supported years. @@ -41,6 +48,8 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * getNextBusinessDay(new Date(2024, 0, 5, 12)); // Mon 2024-01-08, 12:00 (skips the weekend) * getNextBusinessDay(new Date(2024, 0, 6, 12)); // Mon 2024-01-08, 12:00 (from a Saturday) * getNextBusinessDay(new Date(2024, 11, 31, 12)); // Thu 2025-01-02, 12:00 (Jan 1 is Ano novo, skipped) + * getNextBusinessDay(new Date(2024, 0, 5, 12), { includeSaturday: true }); // Sat 2024-01-06, 12:00 (labour count) + * getNextBusinessDay(new Date(2024, 10, 1, 12), { includeSaturday: true }); // Mon 2024-11-04, 12:00 (Nov 2 is Finados, a holiday on a Saturday) * getNextBusinessDay(new Date(2024, 6, 8, 12), { stateCode: "SP" }); // Wed 2024-07-10, 12:00 (Jul 9 is a state holiday in SP) * getNextBusinessDay(new Date("not a date")); // null * getNextBusinessDay(new Date(2099, 11, 31)); // null (the walk leaves the supported years) diff --git a/src/get-nth-business-day/get-nth-business-day.test.ts b/src/get-nth-business-day/get-nth-business-day.test.ts index e341361c..7f417b5c 100644 --- a/src/get-nth-business-day/get-nth-business-day.test.ts +++ b/src/get-nth-business-day/get-nth-business-day.test.ts @@ -172,6 +172,21 @@ describe("getNthBusinessDay", () => { expect(getNthBusinessDay(new Date(2011, 11, 15), 22)).toBeNull(); expect(getNthBusinessDay(new Date(2011, 11, 15), -1)).toEqual(new Date(2011, 11, 29)); }); + + it("should step over the missing Friday 30th with includeSaturday too (-1 is Sat 2011-12-31, -2 is Thu 2011-12-29)", () => { + const options = { includeSaturday: true }; + + expect(getNthBusinessDay(new Date(2011, 11, 15), -1, options)).toEqual( + new Date(2011, 11, 31), + ); + expect(getNthBusinessDay(new Date(2011, 11, 15), -2, options)).toEqual( + new Date(2011, 11, 29), + ); + expect(getNthBusinessDay(new Date(2011, 11, 15), 26, options)).toEqual( + new Date(2011, 11, 31), + ); + expect(getNthBusinessDay(new Date(2011, 11, 15), 27, options)).toBeNull(); + }); }); inTimeZone("UTC", () => { @@ -179,6 +194,17 @@ describe("getNthBusinessDay", () => { expect(getNthBusinessDay(new Date(2011, 11, 15), 22)).toEqual(new Date(2011, 11, 30)); expect(getNthBusinessDay(new Date(2011, 11, 15), 23)).toBeNull(); }); + + it("should count the 27 the same December has with includeSaturday (-2 is Fri 2011-12-30)", () => { + const options = { includeSaturday: true }; + + expect(getNthBusinessDay(new Date(2011, 11, 15), -2, options)).toEqual( + new Date(2011, 11, 30), + ); + expect(getNthBusinessDay(new Date(2011, 11, 15), 27, options)).toEqual( + new Date(2011, 11, 31), + ); + }); }); inTimeZone("America/Havana", () => { @@ -207,6 +233,60 @@ describe("getNthBusinessDay", () => { }); }); + describe("includeSaturday", () => { + it("should give the labour law fifth business day of March 2024 (Wed 2024-03-06, Sat 2024-03-02 counted) against the banking one (Thu 2024-03-07)", () => { + expect(getNthBusinessDay(new Date(2024, 2, 1), 5, { includeSaturday: true })).toEqual( + new Date(2024, 2, 6), + ); + expect(getNthBusinessDay(new Date(2024, 2, 1), 5)).toEqual(new Date(2024, 2, 7)); + }); + + it("should give the labour law fifth business day of February 2024 (Tue 2024-02-06, Sat 2024-02-03 counted) against the banking one (Wed 2024-02-07)", () => { + expect(getNthBusinessDay(new Date(2024, 1, 1), 5, { includeSaturday: true })).toEqual( + new Date(2024, 1, 6), + ); + expect(getNthBusinessDay(new Date(2024, 1, 1), 5)).toEqual(new Date(2024, 1, 7)); + }); + + it("should give the same fifth business day of November 2024 either way (Thu 2024-11-07), since Sat 2024-11-02 is Finados", () => { + expect(getNthBusinessDay(new Date(2024, 10, 1), 5, { includeSaturday: true })).toEqual( + new Date(2024, 10, 7), + ); + expect(getNthBusinessDay(new Date(2024, 10, 1), 5)).toEqual(new Date(2024, 10, 7)); + }); + + it("should skip Independência on Saturday 2024-09-07 and count Sat 2024-09-14 as the 11th (Mon 2024-09-16 without the option)", () => { + expect(getNthBusinessDay(new Date(2024, 8, 1), 11, { includeSaturday: true })).toEqual( + new Date(2024, 8, 14), + ); + expect(getNthBusinessDay(new Date(2024, 8, 1), 11)).toEqual(new Date(2024, 8, 16)); + }); + + it("should give January 2024 its 26 business days (22 plus the Saturdays 6, 13, 20 and 27), the 26th being Wed 2024-01-31", () => { + expect(getNthBusinessDay(new Date(2024, 0, 15), 26, { includeSaturday: true })).toEqual( + new Date(2024, 0, 31), + ); + expect(getNthBusinessDay(new Date(2024, 0, 15), 27, { includeSaturday: true })).toBeNull(); + expect(getNthBusinessDay(new Date(2024, 0, 15), 23)).toBeNull(); + }); + + it("should count backwards from a Saturday at the end of the month (-1 of November 2024 -> Sat 2024-11-30, Fri 2024-11-29 without the option)", () => { + expect(getNthBusinessDay(new Date(2024, 10, 15), -1, { includeSaturday: true })).toEqual( + new Date(2024, 10, 30), + ); + expect(getNthBusinessDay(new Date(2024, 10, 15), -1)).toEqual(new Date(2024, 10, 29)); + }); + + it("should still exclude that Saturday when it is a state holiday (DF, Dia do Evangélico 2024-11-30)", () => { + const result = getNthBusinessDay(new Date(2024, 10, 15), -1, { + stateCode: "DF", + includeSaturday: true, + }); + + expect(result).toEqual(new Date(2024, 10, 29)); + }); + }); + describe("properties", () => { test("should never throw, regardless of the input, prototype chain state codes included", () => { expectNeverThrowsWithArguments( diff --git a/src/get-nth-business-day/get-nth-business-day.ts b/src/get-nth-business-day/get-nth-business-day.ts index 6140a177..54846e55 100644 --- a/src/get-nth-business-day/get-nth-business-day.ts +++ b/src/get-nth-business-day/get-nth-business-day.ts @@ -9,7 +9,8 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * Gets the n-th Brazilian business day (dia útil) of the month a date falls in. * * A business day is a day for which `isBusinessDay` returns `true` (not a Saturday, a Sunday, - * or a Brazilian holiday), evaluated with the same `options`. The month is the one of `date`'s + * or a Brazilian holiday; `options.includeSaturday` keeps Saturday), evaluated with the same + * `options`. The month is the one of `date`'s * **local calendar day** (its local year and month, as read by `Date#getFullYear`/`getMonth`), * the convention every business day util shares; the day of the month and the time of day of * `date` are ignored. @@ -34,9 +35,13 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * Mind the payroll deadline of CLT art. 459 § 1º ("até o quinto dia útil do mês subsequente ao * vencido"): labour inspection counts **Saturday as a business day** for that deadline * (Instrução Normativa MTP nº 2/2021, art. 14, I: "na contagem dos dias será incluído o sábado, - * excluindo-se o domingo e o feriado, inclusive o municipal"), while `isBusinessDay`, and - * therefore this function, never counts a Saturday and does not know municipal holidays. The - * fifth business day returned here is the banking one, which can fall after the labour one. + * excluindo-se o domingo e o feriado, inclusive o municipal"). By default this function gives + * the banking count, Monday to Friday, which can fall after the labour one; pass + * `options.includeSaturday: true` for the labour count, as in + * `getNthBusinessDay(date, 5, { includeSaturday: true })`. A holiday that falls on a Saturday is + * still not counted, exactly as the article says. The one part of the article the option cannot + * cover is "inclusive o municipal": `getHolidays` carries national and state holidays only, so a + * municipal holiday is counted here as an ordinary business day. * * If `options.stateCode` is provided but is not a valid/known state code, it is ignored and only * national holidays are considered (same behavior as `getHolidays`/`isBusinessDay`), so a @@ -48,9 +53,10 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * * @param {Date} date - Any date inside the month to look at. Never mutated: a new `Date` is returned. * @param {number} n - Which business day to get: `1` is the first of the month, `-1` the last. - * @param {BusinessDayOptions} [options] - Which holidays count as non-business days. + * @param {BusinessDayOptions} [options] - Which days count as business days. * @param {StateCode} [options.stateCode] - Brazilian state code whose state holidays are also considered. * @param {boolean} [options.includeOptional] - Whether optional holidays count as non-business days (default: `true`). + * @param {boolean} [options.includeSaturday] - Whether Saturday counts as a business day (default: `false`). * @returns {Date | null} A new `Date` at 00:00 local time of the n-th business day of the month. * `null` on bad input: a `date` that is not a valid `Date` or is outside 1900-2099, an `n` that is * not a finite integer, is `0` or goes beyond the business days of the month, or a `stateCode` @@ -62,6 +68,9 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * getNthBusinessDay(new Date(2024, 0, 15), 5); // Mon 2024-01-08, 00:00 * getNthBusinessDay(new Date(2024, 1, 1), 10); // Thu 2024-02-15, 00:00 (Carnaval, Feb 13, skipped) * getNthBusinessDay(new Date(2024, 1, 1), 10, { includeOptional: false }); // Wed 2024-02-14, 00:00 + * getNthBusinessDay(new Date(2024, 2, 1), 5); // Thu 2024-03-07, 00:00 (banking count) + * getNthBusinessDay(new Date(2024, 2, 1), 5, { includeSaturday: true }); // Wed 2024-03-06, 00:00 (labour count, Mar 2 is a Saturday) + * getNthBusinessDay(new Date(2024, 10, 1), 5, { includeSaturday: true }); // Thu 2024-11-07, 00:00 (Nov 2 is Finados, a holiday on a Saturday) * getNthBusinessDay(new Date(2024, 6, 1), 7, { stateCode: "SP" }); // Wed 2024-07-10, 00:00 (Jul 9 is a state holiday in SP) * getNthBusinessDay(new Date(2024, 0, 15), -1); // Wed 2024-01-31, 00:00 (the last business day) * getNthBusinessDay(new Date(2024, 0, 15), -2); // Tue 2024-01-30, 00:00 @@ -75,7 +84,7 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * CLT art. 459 § 1º, the "quinto dia útil" payroll deadline that motivates the util. * @see Official: https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/inspecao-do-trabalho/areas-de-atuacao/in-2-de-8-denovembro-de-2021.pdf * Instrução Normativa MTP nº 2/2021, art. 14, I: Saturday counts towards that labour deadline, - * which is why the caveat above exists. + * which is what `options.includeSaturday` switches on. * @see Based on: https://unpkg.com/date-fns@4.1.0/lastDayOfMonth.js * Reference for returning the start of the local day and for never mutating the input. The * underlying holiday determination's official sources are cited in `isBusinessDay`/`getHolidays`. diff --git a/src/is-business-day/is-business-day.test.ts b/src/is-business-day/is-business-day.test.ts index 314b179c..aab4eda8 100644 --- a/src/is-business-day/is-business-day.test.ts +++ b/src/is-business-day/is-business-day.test.ts @@ -2,7 +2,8 @@ import * as fc from "fast-check"; import { type StateCode } from "../_internals/constants/states"; import { - businessDayDates, + anyBusinessDayDate, + anyBusinessDayOptions, holidayYears, monthDays, monthIndexes, @@ -19,15 +20,6 @@ function getHolidaysFor(year: number, stateCode: StateCode | null): Holiday[] { return stateCode === null ? getHolidays(year) : getHolidays({ year, stateCode }); } -const anyStateCode = fc.oneof(fc.constantFrom(...PROTOTYPE_KEYS, "SP", "xx"), fc.anything()); -const anyIncludeOptional = fc.oneof(fc.boolean(), fc.anything()); -const hostileOptions = fc.record({ - stateCode: anyStateCode, - includeOptional: anyIncludeOptional, -}); -const anyValueInput = fc.oneof(fc.anything(), businessDayDates); -const anyOptionsInput = fc.oneof(fc.anything(), hostileOptions); - describe("isBusinessDay", () => { it("should return true for a plain weekday that is not a holiday (noon, DST-safe)", () => { expect(isBusinessDay(new Date(2024, 0, 2, 12))).toBe(true); @@ -123,6 +115,54 @@ describe("isBusinessDay", () => { }); }); + describe("includeSaturday", () => { + it("should return true for a plain Saturday when includeSaturday is true (Sat 2024-01-06, IN MTP nº 2/2021 art. 14, I)", () => { + expect(isBusinessDay(new Date(2024, 0, 6, 12), { includeSaturday: true })).toBe(true); + }); + + it("should still return false for a Sunday when includeSaturday is true (Sun 2024-01-07, the article excludes it)", () => { + expect(isBusinessDay(new Date(2024, 0, 7, 12), { includeSaturday: true })).toBe(false); + }); + + it("should still return false for a national holiday that falls on a Saturday (Sat 2024-09-07, Independência)", () => { + expect(isBusinessDay(new Date(2024, 8, 7, 12), { includeSaturday: true })).toBe(false); + }); + + it("should still return false for Finados on Saturday 2024-11-02, and true for the plain Saturday a week later", () => { + expect(isBusinessDay(new Date(2024, 10, 2, 12), { includeSaturday: true })).toBe(false); + expect(isBusinessDay(new Date(2024, 10, 9, 12), { includeSaturday: true })).toBe(true); + }); + + it("should still return false for a state holiday that falls on a Saturday (Sat 2024-11-30, Dia do Evangélico in DF)", () => { + expect( + isBusinessDay(new Date(2024, 10, 30, 12), { stateCode: "DF", includeSaturday: true }), + ).toBe(false); + expect(isBusinessDay(new Date(2024, 10, 30, 12), { includeSaturday: true })).toBe(true); + }); + + it("should leave Monday to Friday untouched (Tue 2024-01-02 is a business day, Mon 2024-01-01 is Ano novo)", () => { + expect(isBusinessDay(new Date(2024, 0, 2, 12), { includeSaturday: true })).toBe(true); + expect(isBusinessDay(new Date(2024, 0, 1, 12), { includeSaturday: true })).toBe(false); + }); + }); + + describe("no breaking change", () => { + it("should keep the Monday to Friday count when includeSaturday is absent, false or undefined (Sat 2024-01-06)", () => { + expect(isBusinessDay(new Date(2024, 0, 6, 12))).toBe(false); + expect(isBusinessDay(new Date(2024, 0, 6, 12), {})).toBe(false); + expect(isBusinessDay(new Date(2024, 0, 6, 12), { includeSaturday: false })).toBe(false); + expect(isBusinessDay(new Date(2024, 0, 6, 12), { includeSaturday: undefined })).toBe(false); + expect(isBusinessDay(new Date(2024, 0, 6, 12), { stateCode: "SP" })).toBe(false); + expect(isBusinessDay(new Date(2024, 0, 6, 12), { includeOptional: false })).toBe(false); + }); + + it("should keep the Monday to Friday count for every Saturday of January 2024 without the option (6, 13, 20 and 27)", () => { + for (const day of [6, 13, 20, 27]) { + expect(isBusinessDay(new Date(2024, 0, day, 12))).toBe(false); + } + }); + }); + describe("year boundaries", () => { it("should return false for 2024-12-31 only if it were a holiday, but treat it as a business day (Tuesday, no holiday)", () => { expect(isBusinessDay(new Date(2024, 11, 31, 12))).toBe(true); @@ -168,9 +208,9 @@ describe("isBusinessDay", () => { }); describe("properties", () => { - const stateAndOptionalArbitrary = fc.tuple(fc.option(stateCodes), fc.boolean()); + const optionsArbitrary = fc.tuple(fc.option(stateCodes), fc.boolean(), fc.boolean()); - test("should return false for every Saturday and Sunday", () => { + test("should return false for every Saturday and Sunday, and still for every Sunday when includeSaturday is true", () => { fc.assert( fc.property(holidayYears, monthIndexes, monthDays, (year, month, day) => { const date = new Date(year, month, day); @@ -178,6 +218,10 @@ describe("isBusinessDay", () => { if (date.getDay() === 0 || date.getDay() === 6) { expect(isBusinessDay(date)).toBe(false); } + + if (date.getDay() === 0) { + expect(isBusinessDay(date, { includeSaturday: true })).toBe(false); + } }), ); }); @@ -188,8 +232,8 @@ describe("isBusinessDay", () => { holidayYears, monthIndexes, monthDays, - stateAndOptionalArbitrary, - (year, month, day, [stateCode, includeOptional]) => { + optionsArbitrary, + (year, month, day, [stateCode, includeOptional, includeSaturday]) => { const date = new Date(year, month, day); const holidays = getHolidaysFor(year, stateCode); const isHolidayMatch = holidays.some( @@ -198,8 +242,12 @@ describe("isBusinessDay", () => { holiday.date.getMonth() === month && holiday.date.getDate() === day, ); - const isWeekend = date.getDay() === 0 || date.getDay() === 6; - const options = { stateCode: stateCode ?? undefined, includeOptional }; + const isWeekend = date.getDay() === 0 || (date.getDay() === 6 && !includeSaturday); + const options = { + stateCode: stateCode ?? undefined, + includeOptional, + includeSaturday, + }; expect(isBusinessDay(date, options)).toBe(!isWeekend && !isHolidayMatch); }, @@ -208,7 +256,7 @@ describe("isBusinessDay", () => { }); test("should never throw, regardless of the input, prototype chain state codes included", () => { - expectNeverThrowsWithOptions(isBusinessDay, anyValueInput, anyOptionsInput); + expectNeverThrowsWithOptions(isBusinessDay, anyBusinessDayDate, anyBusinessDayOptions); }); }); }); @@ -219,6 +267,7 @@ describe("isBusinessDay types", () => { expectTypeOf(isBusinessDay).parameter(1).toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); expectTypeOf(isBusinessDay).returns.toEqualTypeOf(); }); }); diff --git a/src/is-business-day/is-business-day.ts b/src/is-business-day/is-business-day.ts index a918a57d..5a210aee 100644 --- a/src/is-business-day/is-business-day.ts +++ b/src/is-business-day/is-business-day.ts @@ -7,21 +7,26 @@ export type { StateCode } from "../_internals/constants/states"; /** * Options shared by every business day util (`isBusinessDay`, `addBusinessDays`, - * `subBusinessDays` and `differenceInBusinessDays`): which holidays count as non-business days. + * `subBusinessDays`, `differenceInBusinessDays`, `getNextBusinessDay`, `getNthBusinessDay` and + * `getLastBusinessDayOfMonth`): which days count as business days. */ export type BusinessDayOptions = { /** Two letter state code whose state holidays are also treated as non-business days (default: national holidays only). */ stateCode?: StateCode; /** Whether optional-type holidays (`Holiday.type === "optional"`, e.g. Carnaval, Corpus Christi) count as non-business days (default: `true`). */ includeOptional?: boolean; + /** Whether Saturday counts as a business day, the labour law count of Instrução Normativa MTP nº 2/2021, art. 14, I (default: `false`, the Monday to Friday banking count). */ + includeSaturday?: boolean; }; -const WEEKEND_DAYS = new Set([0, 6]); +const SUNDAY = 0; +const SATURDAY = 6; /** * Checks whether a given date is a Brazilian business day (dia útil). * - * A day is not a business day when it falls on Saturday or Sunday, or when it is a + * A day is not a business day when it falls on Saturday or Sunday (`options.includeSaturday` + * keeps Saturday, the labour law count), or when it is a * Brazilian holiday returned by `getHolidays({ year, stateCode })` for `value`'s **local * calendar day** (its local year/month/day, as read by `Date#getFullYear`/`getMonth`/`getDate`), * the same convention used by `isHoliday`. Build `value` from local components @@ -33,6 +38,19 @@ const WEEKEND_DAYS = new Set([0, 6]); * they are not statutory holidays. Pass `false` to only treat statutory (`"national"` and * `"state"`) holidays as non-business days. * + * `options.includeSaturday` defaults to `false`, the Monday to Friday count banks and courts + * use. Pass `true` for the labour law count of the payroll deadline of CLT art. 459 § 1º ("até o + * quinto dia útil do mês subsequente ao vencido"), which the labour inspection reads through + * Instrução Normativa MTP nº 2/2021, art. 14, I: "na contagem dos dias será incluído o sábado, + * excluindo-se o domingo e o feriado, inclusive o municipal". Sunday and holidays are still + * non-business days with the option on, so a holiday that falls on a Saturday stays a + * non-business day. + * + * What `includeSaturday: true` does **not** cover: the "inclusive o municipal" part of that + * rule. `getHolidays` has national and state holidays only, so a municipal holiday is counted as + * a business day here while the labour inspection would exclude it. A count that must be exact + * for a municipality has to remove its municipal holidays on top of this option. + * * An invalid `options.stateCode` is treated in two different ways, depending on its type, the * same split `isHoliday` makes: * @@ -56,9 +74,10 @@ const WEEKEND_DAYS = new Set([0, 6]); * outside it returns `false` rather than silently treating every weekday as a business day. * * @param {Date} value - The date to check. - * @param {BusinessDayOptions} [options] - Which holidays count as non-business days. + * @param {BusinessDayOptions} [options] - Which days count as business days. * @param {StateCode} [options.stateCode] - Brazilian state code whose state holidays are also considered. * @param {boolean} [options.includeOptional] - Whether optional holidays count as non-business days (default: `true`). + * @param {boolean} [options.includeSaturday] - Whether Saturday counts as a business day (default: `false`). * @returns {boolean} True when `value` is a business day, false otherwise. Bad input also * returns false: a `value` that is not a valid `Date` (including non-`Date` values), a * `value` outside the supported 1900-2099 range, or a `stateCode` that is present and is not a @@ -69,6 +88,9 @@ const WEEKEND_DAYS = new Set([0, 6]); * isBusinessDay(new Date(2024, 0, 2)); // true (Tuesday, not a holiday) * isBusinessDay(new Date(2024, 0, 1)); // false (Ano novo) * isBusinessDay(new Date(2024, 0, 6)); // false (Saturday) + * isBusinessDay(new Date(2024, 0, 6), { includeSaturday: true }); // true (labour law count) + * isBusinessDay(new Date(2024, 8, 7), { includeSaturday: true }); // false (Independência, a holiday on a Saturday) + * isBusinessDay(new Date(2024, 0, 7), { includeSaturday: true }); // false (Sunday is never included) * isBusinessDay(new Date(2024, 1, 13)); // false (Carnaval, optional holiday, counted by default) * isBusinessDay(new Date(2024, 1, 13), { includeOptional: false }); // true * isBusinessDay(new Date(2024, 6, 9), { stateCode: "SP" }); // false (Revolução Constitucionalista) @@ -91,7 +113,17 @@ const WEEKEND_DAYS = new Set([0, 6]); * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2023/lei/l14759.htm * Lei 14.759/2023, nationalized Dia da Consciência Negra from 2024. * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9093.htm - * Lei 9.093/1995, the framework law authorizing state and municipal holidays. + * Lei 9.093/1995, the framework law authorizing state and municipal holidays. Its art. 2º leaves + * the up to four feriados religiosos to each municipality's own law, which is why + * `includeSaturday` cannot cover the municipal part of the labour law count: `getHolidays` does + * not carry them. + * @see Official: https://www.planalto.gov.br/ccivil_03/decreto-lei/del5452.htm + * CLT art. 459 § 1º (wording given by Lei 7.855/1989), the "quinto dia útil do mês subsequente ao + * vencido" payroll deadline that `includeSaturday` exists for. + * @see Official: https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/inspecao-do-trabalho/areas-de-atuacao/in-2-de-8-denovembro-de-2021.pdf + * Instrução Normativa MTP nº 2, de 8 de novembro de 2021, art. 14, I: the rule `includeSaturday` + * implements, verbatim "na contagem dos dias será incluído o sábado, excluindo-se o domingo e o + * feriado, inclusive o municipal". * @see Official: https://www.in.gov.br/web/dou/-/portaria-mgi-n-11.460-de-29-de-dezembro-de-2025-678388627 * Portaria MGI nº 11.460/2025, the federal executive's annual calendar of feriados nacionais and * pontos facultativos: the source of three of the four Easter-derived entries, namely @@ -111,7 +143,13 @@ export const isBusinessDay = (value: Date, options?: BusinessDayOptions): boolea if (!isSupportedHolidayYear(year)) return false; - if (WEEKEND_DAYS.has(value.getDay())) return false; + const day = value.getDay(); + + if (day === SUNDAY) return false; + + const includeSaturday = options?.includeSaturday ?? false; + + if (day === SATURDAY && !includeSaturday) return false; const includeOptional = options?.includeOptional ?? true; diff --git a/src/sub-business-days/sub-business-days.test.ts b/src/sub-business-days/sub-business-days.test.ts index 8de347ae..783ff7a1 100644 --- a/src/sub-business-days/sub-business-days.test.ts +++ b/src/sub-business-days/sub-business-days.test.ts @@ -113,6 +113,32 @@ describe("subBusinessDays", () => { }); }); + describe("includeSaturday", () => { + it("should stop on Saturday when it counts (Mon 2024-01-08 - 1 -> Sat 2024-01-06)", () => { + const result = subBusinessDays(new Date(2024, 0, 8, 12), 1, { includeSaturday: true }); + + expect(result).toEqual(new Date(2024, 0, 6, 12)); + }); + + it("should keep walking back to Friday without the option (Mon 2024-01-08 - 1 -> Fri 2024-01-05)", () => { + expect(subBusinessDays(new Date(2024, 0, 8, 12), 1, { includeSaturday: false })).toEqual( + new Date(2024, 0, 5, 12), + ); + }); + + it("should still walk back over a holiday that falls on a Saturday (Mon 2024-11-04 - 1 -> Fri 2024-11-01, Finados)", () => { + const result = subBusinessDays(new Date(2024, 10, 4, 12), 1, { includeSaturday: true }); + + expect(result).toEqual(new Date(2024, 10, 1, 12)); + }); + + it("should walk forwards onto Saturday for a negative amount (Fri 2024-01-05 - -1 -> Sat 2024-01-06)", () => { + const result = subBusinessDays(new Date(2024, 0, 5, 12), -1, { includeSaturday: true }); + + expect(result).toEqual(new Date(2024, 0, 6, 12)); + }); + }); + describe("invalid input", () => { for (const [label, call] of NULL_CALLS) { it(`should return null for ${label}`, () => { @@ -125,6 +151,12 @@ describe("subBusinessDays", () => { it("should walk back over 30 December 2011, the local day Samoa skipped to cross the date line", () => { expect(subBusinessDays(new Date(2012, 0, 5, 12), 4)).toEqual(new Date(2011, 11, 29, 12)); }); + + it("should walk back over it with includeSaturday too, counting Sat 2011-12-31 and landing on Thu 2011-12-29", () => { + expect(subBusinessDays(new Date(2012, 0, 2, 12), 2, { includeSaturday: true })).toEqual( + new Date(2011, 11, 29, 12), + ); + }); }); inTimeZone("America/Sao_Paulo", () => { diff --git a/src/sub-business-days/sub-business-days.ts b/src/sub-business-days/sub-business-days.ts index 664c6fbd..e3449218 100644 --- a/src/sub-business-days/sub-business-days.ts +++ b/src/sub-business-days/sub-business-days.ts @@ -8,9 +8,9 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * * The mirror image of `addBusinessDays`, which it delegates to: `subBusinessDays(date, amount)` * is `addBusinessDays(date, -amount)`, down to the last detail. A business day is a day for - * which `isBusinessDay` returns `true` (not a Saturday, a Sunday, or a Brazilian holiday), - * evaluated with the same `options`, and the walk goes one calendar day at a time, counting only - * business days. + * which `isBusinessDay` returns `true` (not a Saturday, a Sunday, or a Brazilian holiday; + * `options.includeSaturday` keeps Saturday), evaluated with the same `options`, and the walk goes + * one calendar day at a time, counting only business days. * * `amount: 0` returns a **new `Date` equal to `date`, unchanged**, even when `date` itself falls * on a weekend or holiday, and a negative `amount` walks *forwards*, exactly like date-fns' @@ -19,6 +19,12 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * The time-of-day (hours, minutes, seconds, milliseconds) of `date` is preserved in the result, * and `date` itself is never mutated. * + * `options.includeSaturday` defaults to `false`, the Monday to Friday banking count. Pass `true` + * for the labour law count of Instrução Normativa MTP nº 2/2021, art. 14, I, which includes + * Saturday and still excludes Sunday and holidays, so a holiday that falls on a Saturday is never + * counted. See `isBusinessDay` for the law behind it and for what it does not cover: municipal + * holidays, which `getHolidays` does not carry. + * * If `options.stateCode` is provided but is not a valid/known state code, it is ignored and only * national holidays are considered (same behavior as `getHolidays`/`isBusinessDay`), so a * prototype-chain key such as `"__proto__"` is an unknown state code like any other. An `options` @@ -29,9 +35,10 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * * @param {Date} date - The date to count from. Never mutated: a new `Date` is returned. * @param {number} amount - The number of business days to subtract; a negative value walks forwards. - * @param {BusinessDayOptions} [options] - Which holidays count as non-business days. + * @param {BusinessDayOptions} [options] - Which days count as business days. * @param {StateCode} [options.stateCode] - Brazilian state code whose state holidays are also considered. * @param {boolean} [options.includeOptional] - Whether optional holidays count as non-business days (default: `true`). + * @param {boolean} [options.includeSaturday] - Whether Saturday counts as a business day (default: `false`). * @returns {Date | null} A new `Date`, `amount` business days before `date`. `null` on bad input: * a `date` that is not a valid `Date` or is outside 1900-2099, an `amount` that is not a finite * integer, a `stateCode` that is not a string, or a walk that leaves the supported years. @@ -43,6 +50,8 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * subBusinessDays(new Date(2025, 0, 2, 12), 1); // Tue 2024-12-31, 12:00 (Jan 1 is Ano novo, skipped) * subBusinessDays(new Date(2024, 0, 5, 12), -1); // Mon 2024-01-08, 12:00 (walks forwards) * subBusinessDays(new Date(2024, 0, 6, 12), 0); // Sat 2024-01-06, 12:00 (unchanged, even though Saturday is not a business day) + * subBusinessDays(new Date(2024, 0, 8, 12), 1, { includeSaturday: true }); // Sat 2024-01-06, 12:00 (labour count) + * subBusinessDays(new Date(2024, 10, 4, 12), 1, { includeSaturday: true }); // Fri 2024-11-01, 12:00 (Nov 2 is Finados, a holiday on a Saturday) * subBusinessDays(new Date(2024, 6, 10, 12), 1, { stateCode: "SP" }); // Mon 2024-07-08, 12:00 (Jul 9 is a state holiday in SP) * subBusinessDays(new Date("not a date"), 1); // null * subBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer)