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
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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

Port datetime package (timezone-aware date helpers and date-range presets) as an internal package from next-woocommerce-analytics.
16 changes: 16 additions & 0 deletions projects/packages/premium-analytics/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { makeBaseConfig, defineConfig } from 'jetpack-js-tools/eslintrc/base.mjs';

/**
* Soften JSDoc rules for `packages/datetime/**` so the initial port can
* land. Temporary — backfill proper descriptions on the helpers and
* remove this override (at which point this whole file can go away).
*/
export default defineConfig( makeBaseConfig( import.meta.url ), {
files: [ 'packages/datetime/**' ],
rules: {
'jsdoc/require-description': 'off',
'jsdoc/require-param-description': 'off',
'jsdoc/require-returns': 'off',
'jsdoc/check-indentation': 'off',
},
} );
2 changes: 2 additions & 0 deletions projects/packages/premium-analytics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
}
},
"dependencies": {
"@date-fns/tz": "1.4.1",
"@wordpress/boot": "0.13.0",
"@wordpress/data": "10.46.0",
"@wordpress/i18n": "^6.9.0",
"@wordpress/icons": "^13.0.0",
"@wordpress/route": "0.12.0",
"date-fns": "4.1.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
Expand Down
129 changes: 129 additions & 0 deletions projects/packages/premium-analytics/packages/datetime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# @automattic/jetpack-premium-analytics-datetime

Date and timezone utilities for Jetpack Premium Analytics.

## Overview

Provides timezone-aware date handling and comparison range calculations
for analytics widgets and date-range pickers.

## Functions

### Timezone Utilities

#### `createTZDateFromParts( dateParts: number[], timezone? )`

Creates a timezone-aware date in the specified timezone using the provided date parts.
**Important:** Months are zero-based (0 = January, 11 = December).

```ts
// October 09, 2025 00:00 AM in America/New_York time
const date = createTZDateFromParts( [ 2025, 9, 9 ], 'America/New_York' );
```

**Parameters:**

- `dateParts` : `number[]` - Date value to convert
- `timezone` (optional): `string` - Target timezone, default is GMT

**Returns:** `TZDate` - Timezone-aware date object

#### `toLocalTZ( value?, timezone? )`

Creates a timezone-aware date in the specified timezone.

```typescript
const date = toLocalTZ( '2024-01-15', 'America/New_York' );
const now = toLocalTZ( undefined, '+05:30' ); // Current time in +05:30
```

**Parameters:**

- `value` (optional): `number | string | Date` - Date value to convert
- `timezone` (optional): `string` - Target timezone

**Returns:** `TZDate` - Timezone-aware date object

#### `formatToTimezoneNaiveString( date, timezone )`

Formats a date to an ISO string without timezone offset.

```typescript
const naive = formatToTimezoneNaiveString( new Date(), 'Europe/London' );
// Returns: "2024-01-15T14:30:00.000"
```

**Parameters:**

- `date`: `Date` - Date to format
- `timezone`: `string` - Timezone for interpretation

**Returns:** `string` - ISO string without timezone offset

#### `dateToISOStringWithTZ( date, timezone )`

Converts a date to ISO string with timezone offset applied.

```typescript
const withTZ = dateToISOStringWithTZ( new Date(), 'America/New_York' );
// Returns: "2024-01-15T14:30:00.000-05:00"
```

**Parameters:**

- `date`: `Date` - Date to convert
- `timezone`: `string` - Target timezone

**Returns:** `string` - ISO string with timezone offset

### Comparison Range Calculations

#### `getComparisonRangeFromPreset( reference, presetId )`

Calculates comparison date ranges based on predefined presets.

```typescript
const reference = {
from: new Date( '2024-01-15' ),
to: new Date( '2024-01-21' ),
};
const comparison = getComparisonRangeFromPreset( reference, 'previous-week' );
// Returns dates for Jan 8-14, 2024
```

**Parameters:**

- `reference`: `DateRange` - Reference date range with `from` and `to`
- `presetId`: `ComparisonPresetId` - One of the supported preset identifiers

**Returns:** `DateRange | undefined` - Comparison date range or undefined
if inputs are invalid

**Supported presets:**

- `previous-period` - Same duration, immediately before reference
- `previous-week` - One week before reference dates
- `previous-month` - One month before reference dates
- `previous-year` - One year before reference dates

## Types

### `DateRange`

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

### `ComparisonPresetId`

```typescript
type ComparisonPresetId = 'previous-period' | 'previous-week' | 'previous-month' | 'previous-year';
```

## Dependencies

- `date-fns` - Date manipulation functions
- `@date-fns/tz` - Timezone support for date-fns
14 changes: 14 additions & 0 deletions projects/packages/premium-analytics/packages/datetime/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@automattic/jetpack-premium-analytics-datetime",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"sideEffects": false,
"dependencies": {
"@date-fns/tz": "1.4.1",
"@wordpress/i18n": "^6.9.0",
"date-fns": "4.1.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* External dependencies
*/
import {
differenceInDays,
subDays,
subWeeks,
subMonths,
subYears,
startOfDay,
endOfDay,
} from 'date-fns';

/**
* Supported comparison preset identifiers.
*/
export type DateRange = { from?: Date; to?: Date };

/**
* Named constants for comparison preset identifiers.
*/
export const COMPARISON_PREVIOUS_PERIOD = 'previous-period' as const;
export const COMPARISON_PREVIOUS_WEEK = 'previous-week' as const;
export const COMPARISON_PREVIOUS_MONTH = 'previous-month' as const;
export const COMPARISON_PREVIOUS_YEAR = 'previous-year' as const;

/**
* All comparison preset identifiers, in display order.
*/
export const COMPARISON_PRESETS = [
COMPARISON_PREVIOUS_PERIOD,
COMPARISON_PREVIOUS_WEEK,
COMPARISON_PREVIOUS_MONTH,
COMPARISON_PREVIOUS_YEAR,
] as const;

export type ComparisonPresetId = ( typeof COMPARISON_PRESETS )[ number ];

/**
* Type guard to check if a string is a valid ComparisonPresetId.
*
* @param value - The value to check.
* @return True if the value is a valid ComparisonPresetId, false otherwise.
*/
export function isComparisonPresetId( value: unknown ): value is ComparisonPresetId {
return typeof value === 'string' && ( COMPARISON_PRESETS as readonly string[] ).includes( value );
}

/**
* Returns a comparison DateRange (as Date objects) derived from a reference range
* and a given preset.
*
* - This function is pure and has no side effects.
* - It does not apply any timezone adjustments. The caller is responsible for
* normalizing dates to the desired local day boundaries before passing them in.
*
* @param reference - The reference range to compare against (must include both `from` and `to`).
* @param presetId - One of the supported preset identifiers.
* @return A new DateRange for the comparison period, or `undefined` if inputs are invalid.
*/
export function getComparisonRangeFromPreset(
reference: DateRange,
presetId: ComparisonPresetId
): DateRange | undefined {
if ( ! reference?.from || ! reference?.to ) {
return undefined;
}

const refFrom = reference.from;
const refTo = reference.to;

const clampDayBound = ( date: Date, bound: 0 | 1 ) =>
bound === 1 ? endOfDay( startOfDay( date ) ) : startOfDay( date );

if ( presetId === COMPARISON_PREVIOUS_PERIOD ) {
const daysInclusive = differenceInDays( refTo, refFrom ) + 1;
return {
from: clampDayBound( subDays( refFrom, daysInclusive ), 0 ),
to: clampDayBound( subDays( refTo, daysInclusive ), 1 ),
};
}

if ( presetId === COMPARISON_PREVIOUS_WEEK ) {
return {
from: clampDayBound( subWeeks( refFrom, 1 ), 0 ),
to: clampDayBound( subWeeks( refTo, 1 ), 1 ),
};
}

if ( presetId === COMPARISON_PREVIOUS_MONTH ) {
return {
from: clampDayBound( subMonths( refFrom, 1 ), 0 ),
to: clampDayBound( subMonths( refTo, 1 ), 1 ),
};
}

if ( presetId === COMPARISON_PREVIOUS_YEAR ) {
return {
from: clampDayBound( subYears( refFrom, 1 ), 0 ),
to: clampDayBound( subYears( refTo, 1 ), 1 ),
};
}

return undefined;
}
49 changes: 49 additions & 0 deletions projects/packages/premium-analytics/packages/datetime/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export {
getComparisonRangeFromPreset,
isComparisonPresetId,
type DateRange,
type ComparisonPresetId,
} from './get-comparison-range';

export {
createTZDateFromParts,
toLocalTZ,
formatToTimezoneNaiveString,
dateToISOStringWithTZ,
startOfDayTZ,
endOfDayTZ,
} from './tz';

export {
// Constants
SELECTABLE_PRESETS,
PRESET_TODAY,
PRESET_YESTERDAY,
PRESET_LAST_7_DAYS,
PRESET_LAST_30_DAYS,
PRESET_LAST_90_DAYS,
PRESET_LAST_365_DAYS,
PRESET_LAST_MONTH,
PRESET_LAST_12_MONTHS,
PRESET_LAST_YEAR,
PRESET_CUSTOM,

// Guards
isSelectablePreset,
isPrimaryPreset,

// Types
type SelectablePresetId,
type PrimaryPresetId,

// Primary presets
PRESET_DEFINITIONS,
getPresetLabel,
getDefaultDateRangePresets,
computePrimaryRange,
type DateRangePreset,

// Comparison presets
getComparisonPresetLabel,
getComparisonPresetConfigs,
} from './presets';
Loading
Loading