Skip to content
Open
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
7 changes: 2 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,9 +368,6 @@ An inline `@phan-suppress-next-line <Rule> -- <reason>` is acceptable ONLY as a
- The "phan: Update wpcom stubs" PR is machine-generated and gets rebased/recreated on every job run — never hand-edit it, your changes will be overwritten.
- `.phan/stubs/wpcom-stubs.php` is likewise generated (its header says so). Never edit it directly to add a symbol — add it to `stub-defs.php` in wpcom instead.

## Maintaining this file
## Maintaining This File

Keep this file for knowledge useful to almost every future agent session in this project.
Do not repeat what the codebase already shows; point to the authoritative file or command instead.
Prefer rewriting or pruning existing entries over appending new ones.
When updating this file, preserve this bar for all agents and keep entries concise.
If you discover a pattern or pitfall not covered here, mention it to the developer so they can decide whether to update this file.
2 changes: 0 additions & 2 deletions CLAUDE.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Date range: Type the range bounds as zoned instants, so day boundaries are measured on the site's clock. A range whose end precedes its start now reports no period length instead of a one-hour window.
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ Calculates comparison date ranges based on predefined presets.

```typescript
const reference = {
from: new Date( '2024-01-15' ),
to: new Date( '2024-01-21' ),
from: localTZDate( '2024-01-15', 'America/New_York' ),
to: localTZDate( '2024-01-21', 'America/New_York' ),
};
const comparison = getComparisonRangeFromPreset( reference, 'previous-period' );
// Returns dates for Jan 8-14, 2024
Expand Down Expand Up @@ -221,11 +221,14 @@ twelve-month window as 12 months.

```typescript
type DateRange = {
from?: Date;
to?: Date;
from?: TZDate;
to?: TZDate;
};
```

Both bounds stay optional: `resolveBucketStamp` returns `undefined` for a bound
it cannot resolve, and the chart passes that straight through.

### `ComparisonPresetId`

```typescript
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,46 @@
*/
import { COMPARISON_PRESETS, isComparisonPresetId } from '../get-comparison-range';
import { getComparisonOptions } from '../presets';
import { createTZDateFromParts } from '../tz';
import type { TZDate } from '@date-fns/tz';
/**
* A site timezone with a fixed offset, so every expectation below holds
* whatever timezone the machine running the suite is in.
*/
const SITE_ZONE = 'Asia/Taipei';

/**
* Build a site-local date from the parts `new Date()` takes.
*
* @param year - Full year.
* @param month - 0-indexed month.
* @param day - Day of month.
* @param hours - Hour of day.
* @param minutes - Minute of hour.
* @param seconds - Second of minute.
* @param ms - Millisecond of second.
* @return The date.
*/
function siteDate(
year: number,
month: number,
day: number,
hours = 0,
minutes = 0,
seconds = 0,
ms = 0
): TZDate {
return createTZDateFromParts( [ year, month, day, hours, minutes, seconds, ms ], SITE_ZONE );
}

/**
* A day-aligned range, inclusive on both ends. Months are 0-based.
* @param from
* @param to
*/
const daysRange = ( from: [ number, number, number ], to: [ number, number, number ] ) => ( {
from: new Date( from[ 0 ], from[ 1 ], from[ 2 ], 0, 0, 0, 0 ),
to: new Date( to[ 0 ], to[ 1 ], to[ 2 ], 23, 59, 59, 999 ),
from: siteDate( from[ 0 ], from[ 1 ], from[ 2 ], 0, 0, 0, 0 ),
to: siteDate( to[ 0 ], to[ 1 ], to[ 2 ], 23, 59, 59, 999 ),
} );

const ids = ( range: Parameters< typeof getComparisonOptions >[ 0 ] ) =>
Expand Down Expand Up @@ -42,7 +73,7 @@ describe( 'comparison options', () => {

it( 'returns nothing for an incomplete or inverted range', () => {
expect( getComparisonOptions( {} ) ).toEqual( [] );
expect( getComparisonOptions( { from: new Date( 2026, 7, 30 ) } ) ).toEqual( [] );
expect( getComparisonOptions( { from: siteDate( 2026, 7, 30 ) } ) ).toEqual( [] );
expect( getComparisonOptions( daysRange( [ 2026, 7, 30 ], [ 2026, 7, 29 ] ) ) ).toEqual( [] );
} );

Expand All @@ -65,8 +96,8 @@ describe( 'comparison options', () => {

it( 'reads a rolling 24-hour window in hours', () => {
const last24Hours = {
from: new Date( 2026, 7, 30, 15, 0, 0, 0 ),
to: new Date( 2026, 7, 31, 14, 59, 59, 999 ),
from: siteDate( 2026, 7, 30, 15, 0, 0, 0 ),
to: siteDate( 2026, 7, 31, 14, 59, 59, 999 ),
};

const options = getComparisonOptions( last24Hours );
Expand All @@ -79,8 +110,8 @@ describe( 'comparison options', () => {
] );
expect( options[ 0 ].label ).toBe( 'Previous 24 hours' );
expect( options[ 1 ].range ).toEqual( {
from: new Date( 2026, 7, 23, 15, 0, 0, 0 ),
to: new Date( 2026, 7, 24, 14, 59, 59, 999 ),
from: siteDate( 2026, 7, 23, 15, 0, 0, 0 ),
to: siteDate( 2026, 7, 24, 14, 59, 59, 999 ),
} );
} );

Expand Down Expand Up @@ -199,16 +230,16 @@ describe( 'comparison options', () => {

it( 'reads a drilled single hour as the previous hour', () => {
const hour = {
from: new Date( 2026, 7, 31, 14, 0, 0, 0 ),
to: new Date( 2026, 7, 31, 14, 59, 59, 999 ),
from: siteDate( 2026, 7, 31, 14, 0, 0, 0 ),
to: siteDate( 2026, 7, 31, 14, 59, 59, 999 ),
};

const options = getComparisonOptions( hour );

expect( options[ 0 ].label ).toBe( 'Previous hour' );
expect( options[ 0 ].range ).toEqual( {
from: new Date( 2026, 7, 31, 13, 0, 0, 0 ),
to: new Date( 2026, 7, 31, 13, 59, 59, 999 ),
from: siteDate( 2026, 7, 31, 13, 0, 0, 0 ),
to: siteDate( 2026, 7, 31, 13, 59, 59, 999 ),
} );
} );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@
* Internal dependencies
*/
import { getDateRangeSpan } from '../date-range-span';
import { createTZDateFromParts } from '../tz';
import type { TZDate } from '@date-fns/tz';

/**
* Build a local-time date. Dates are constructed from parts rather than parsed
* from ISO strings so the day boundaries land in the machine's timezone, which
* is the frame `date-fns` reads.
* A site timezone with a fixed offset, so every expectation below holds
* whatever timezone the machine running the suite is in.
*/
const SITE_ZONE = 'Asia/Taipei';

/**
* Build a site-local date.
*
* @param year - Full year.
* @param month - 1-indexed month.
Expand All @@ -15,8 +21,8 @@ import { getDateRangeSpan } from '../date-range-span';
* @param minutes - Minute of hour.
* @return The date.
*/
function at( year: number, month: number, day: number, hours = 0, minutes = 0 ): Date {
return new Date( year, month - 1, day, hours, minutes, 0, 0 );
function at( year: number, month: number, day: number, hours = 0, minutes = 0 ): TZDate {
return createTZDateFromParts( [ year, month - 1, day, hours, minutes, 0, 0 ], SITE_ZONE );
}

/**
Expand All @@ -27,8 +33,8 @@ function at( year: number, month: number, day: number, hours = 0, minutes = 0 ):
* @param day - Day of month.
* @return The end of that day.
*/
function endOf( year: number, month: number, day: number ): Date {
return new Date( year, month - 1, day, 23, 59, 59, 999 );
function endOf( year: number, month: number, day: number ): TZDate {
return createTZDateFromParts( [ year, month - 1, day, 23, 59, 59, 999 ], SITE_ZONE );
}

describe( 'getDateRangeSpan', () => {
Expand All @@ -38,6 +44,10 @@ describe( 'getDateRangeSpan', () => {
expect( getDateRangeSpan( { from: at( 2026, 7, 21 ) } ) ).toBeNull();
} );

it( 'returns null when the range runs backwards', () => {
expect( getDateRangeSpan( { from: endOf( 2026, 7, 28 ), to: at( 2026, 6, 29 ) } ) ).toBeNull();
} );

it( 'counts whole days inclusively', () => {
expect( getDateRangeSpan( { from: at( 2026, 7, 21 ), to: endOf( 2026, 7, 27 ) } ) ).toEqual( {
unit: 'day',
Expand All @@ -61,7 +71,7 @@ describe( 'getDateRangeSpan', () => {
expect(
getDateRangeSpan( {
from: at( 2026, 7, 28, 19 ),
to: new Date( 2026, 6, 29, 18, 59, 59, 999 ),
to: createTZDateFromParts( [ 2026, 6, 29, 18, 59, 59, 999 ], SITE_ZONE ),
} )
).toEqual( { unit: 'hour', value: 24 } );
} );
Expand Down
Loading
Loading