Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions homedocs/src/data/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -1455,6 +1455,16 @@ export const navigation = [
slug: "parse-date-invariant",
description: "Parse date in invariant format",
},
{
title: "dayBefore",
slug: "day-before",
description: "Get the previous calendar day",
},
{
title: "dateQuarter",
slug: "date-quarter",
description: "Get the calendar quarter of a date",
},
],
},
{
Expand Down
19 changes: 19 additions & 0 deletions homedocs/src/pages/docs/intro/formatting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,25 @@ The `daybefore` format renders a date shifted back by one calendar day. It accep
| `daybefore;MMM yyyy` | `2021-01-01` | `Dec 2020` |
| `daybefore;yyyyMMdd` | `2021-01-01` | `12/31/2020` |

## Quarter Format

The `quarter` format renders the calendar quarter of a date. Its pattern is a string template with `{q}` (quarter number, 1–4), `{yyyy}` (4-digit year), and `{yy}` (2-digit year) placeholders; any other text is literal. Placeholders are case-insensitive — `{Q}`, `{YYYY}`, and `{YY}` work too.

| Format | Example Input | Example Output |
| ------------------ | ------------- | -------------- |
| `quarter` | `2020-02-15` | `Q1 2020` |
| `quarter;{yy}Q{q}` | `2020-02-15` | `20Q1` |
| `quarter;{q}Q{yy}` | `2020-02-15` | `1Q20` |

Add `exclusive` (or the short forms `ex` / `e`) as the last argument to treat the date as the **exclusive end** of a range. It steps back one calendar day before computing the quarter, so a half-open range from `2020-01-01` up to `2021-01-01` displays as `20Q1 - 20Q4`:

| Format | Example Input | Example Output |
| ----------------------------- | ------------- | -------------- |
| `quarter;{yy}Q{q};exclusive` | `2021-01-01` | `20Q4` |
| `quarter;;e` | `2021-01-01` | `Q4 2020` |

The last row shows the empty middle argument falling back to the default pattern while still applying the exclusive flag.

## Programmatic Formatting

Use `Format.value()` to format values in code:
Expand Down
50 changes: 50 additions & 0 deletions homedocs/src/pages/docs/utilities/date-quarter.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
layout: ../../../layouts/DocsLayout.astro
title: dateQuarter
description: Get the calendar quarter of a date
---

import ImportPath from "../../../components/ImportPath.astro";
import OnThisPage from "../../../components/OnThisPage.astro";

# dateQuarter

<ImportPath path="import { dateQuarter } from 'cx/util';" />
<OnThisPage />

Returns the calendar quarter (`1`–`4`) the given date falls in.

## Signature

```ts
function dateQuarter(date: Date): number
```

## Examples

```ts
dateQuarter(new Date(2024, 0, 15)); // 1 — January
dateQuarter(new Date(2024, 5, 1)); // 2 — June
dateQuarter(new Date(2024, 8, 30)); // 3 — September
dateQuarter(new Date(2024, 11, 31)); // 4 — December
```

## Use Cases

### Grouping records by quarter

```ts
const byQuarter: Record<number, Sale[]> = {};
for (const sale of sales) {
const q = dateQuarter(sale.date);
if (!byQuarter[q]) byQuarter[q] = [];
byQuarter[q].push(sale);
}
```

To render quarters for display, see the `quarter` [format](/docs/intro/formatting#quarter-format).

## See Also

- [monthStart](/docs/utilities/month-start) - Get first day of month
- [dayBefore](/docs/utilities/day-before) - Get the previous calendar day
56 changes: 56 additions & 0 deletions homedocs/src/pages/docs/utilities/day-before.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
layout: ../../../layouts/DocsLayout.astro
title: dayBefore
description: Get the previous calendar day
---

import ImportPath from "../../../components/ImportPath.astro";
import OnThisPage from "../../../components/OnThisPage.astro";

# dayBefore

<ImportPath path="import { dayBefore } from 'cx/util';" />
<OnThisPage />

Returns a new `Date` representing the calendar day before the given date, keeping the same time of day. Month and year boundaries are handled automatically, and the input is not mutated.

## Signature

```ts
function dayBefore(date: Date): Date
```

## Examples

```ts
dayBefore(new Date(2021, 0, 1));
// 2020-12-31 — steps across the year boundary

dayBefore(new Date(2024, 2, 1, 9, 30));
// 2024-02-29 09:30 — handles the leap day, keeps the time

// The input is unchanged
const date = new Date(2024, 5, 15);
dayBefore(date);
console.log(date.getDate()); // 15
```

## Use Cases

### Displaying an exclusive range end

Date ranges are often stored half-open, with an exclusive end. `dayBefore` converts that exclusive end into the last day actually contained in the range.

```ts
// Range [2020-01-01, 2021-01-01) shown to the user as 2020-01-01 – 2020-12-31
const exclusiveTo = new Date(2021, 0, 1);
const inclusiveTo = dayBefore(exclusiveTo); // 2020-12-31
```

To render such values directly, see the `daybefore` [format](/docs/intro/formatting#day-before-format).

## See Also

- [dateDiff](/docs/utilities/date-diff) - Calculate date difference
- [zeroTime](/docs/utilities/zero-time) - Set time to midnight
- [dateQuarter](/docs/utilities/date-quarter) - Get the calendar quarter of a date
32 changes: 32 additions & 0 deletions packages/cx/src/ui/Format.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import assert from "assert";
import { Format } from "./Format";

// The `quarter` formatter is registered eagerly when ui/Format is imported,
// so these tests do not need enableCultureSensitiveFormatting().
describe("Format - quarter", function () {
it("renders the calendar quarter with the default pattern", function () {
assert.equal(Format.value(new Date(2020, 0, 15), "quarter"), "Q1 2020");
assert.equal(Format.value(new Date(2020, 5, 1), "quarter"), "Q2 2020");
assert.equal(Format.value(new Date(2020, 11, 31), "quarter"), "Q4 2020");
});

it("supports custom patterns and the {yy} token", function () {
assert.equal(Format.value(new Date(2020, 0, 1), "quarter;{yy}Q{q}"), "20Q1");
assert.equal(Format.value(new Date(2020, 8, 1), "quarter;{q}Q{yyyy}"), "3Q2020");
});

it("accepts both lowercase and uppercase placeholders", function () {
assert.equal(Format.value(new Date(2020, 0, 1), "quarter;{YY}Q{Q}"), "20Q1");
assert.equal(Format.value(new Date(2020, 0, 1), "quarter;Q{Q} {YYYY}"), "Q1 2020");
});

it("treats the input as an exclusive range end with the exclusive flag", function () {
assert.equal(Format.value(new Date(2021, 0, 1), "quarter;{yy}Q{q};exclusive"), "20Q4");
assert.equal(Format.value(new Date(2021, 0, 1), "quarter;{yy}Q{q};ex"), "20Q4");
});

it("falls back to the default pattern when the pattern argument is empty (quarter;;e)", function () {
assert.equal(Format.value(new Date(2021, 0, 1), "quarter;;e"), "Q4 2020");
assert.equal(Format.value(new Date(2020, 0, 1), "quarter;;e"), "Q4 2019");
});
});
23 changes: 21 additions & 2 deletions packages/cx/src/ui/Format.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import { Culture, getCurrentCultureCache } from "./Culture";
import { Format as Fmt, resolveMinMaxFractionDigits, setGetFormatCacheCallback } from "../util/Format";
import { setGetExpressionCacheCallback } from "../data/Expression";
import { setGetStringTemplateCacheCallback } from "../data/StringTemplate";
import { dayBefore, parseDateInvariant } from "../util";
import { setGetStringTemplateCacheCallback, StringTemplate } from "../data/StringTemplate";
import { dateQuarter, dayBefore, parseDateInvariant } from "../util";
import { GlobalCacheIdentifier } from "../util/GlobalCacheIdentifier";

export const Format = Fmt;

// The `quarter` formatter renders a calendar quarter via a string-template
// pattern with `{q}` (quarter number), `{yyyy}` and `{yy}` (year) placeholders.
// Placeholders are case-insensitive (`{Q}`, `{YYYY}`, `{YY}` work too). It lives
// here rather than in util/Format because it depends on StringTemplate from the
// data layer, which util/ cannot import. It is registered eagerly since it does
// not depend on culture settings.
Fmt.registerFactory("quarter", (fmt: any, pattern?: string, mode?: string) => {
let exclusive = mode === "exclusive" || mode === "ex" || mode === "e";
let template = StringTemplate.get(pattern || "Q{q} {yyyy}");
return (value: any) => {
let date = parseDateInvariant(value);
if (exclusive) date = dayBefore(date);
let q = dateQuarter(date);
let yyyy = date.getFullYear();
let yy = String(yyyy % 100).padStart(2, "0");
return template({ q, Q: q, yyyy, YYYY: yyyy, yy, YY: yy });
};
});

let cultureSensitiveFormatsRegistered = false;

export function resolveNumberFormattingFlags(flags?: string): any {
Expand Down
8 changes: 8 additions & 0 deletions packages/cx/src/util/date/dateQuarter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Returns the calendar quarter (1-4) the given date falls in.
* @param date
* @returns {number}
*/
export function dateQuarter(date: Date): number {
return Math.floor(date.getMonth() / 3) + 1;
}
1 change: 1 addition & 0 deletions packages/cx/src/util/date/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./dateDiff";
export * from "./dateQuarter";
export * from "./dayBefore";
export * from "./zeroTime";
export * from "./monthStart";
Expand Down
Loading