Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
00fed2d
feat(premium-analytics): add tsconfig paths and typecheck for interna…
chihsuan May 27, 2026
20e6241
Potential fix for pull request finding
chihsuan May 27, 2026
662c887
Potential fix for pull request finding
chihsuan May 27, 2026
d4da9ca
docs(premium-analytics): clarify internal-package naming and rename init
chihsuan May 28, 2026
c2b0fae
chore(premium-analytics): copy formatters package from next-woocommer…
chihsuan May 28, 2026
5c2cd81
chore(premium-analytics): adapt formatters package.json for internal-…
chihsuan May 28, 2026
a0a3a41
chore(premium-analytics): pin formatters deps for Jetpack monorepo
chihsuan May 28, 2026
27b23d2
docs(premium-analytics): adapt formatters README and code comments fo…
chihsuan May 28, 2026
390d7d8
chore(premium-analytics): wire formatters deps into parent and relax …
chihsuan May 28, 2026
ae03177
chore(premium-analytics): format formatters and clean up lint issues
chihsuan May 28, 2026
216f6e2
changelog(premium-analytics): add entry for WOOA7S-1313 formatters port
chihsuan May 28, 2026
d8ad326
chore(premium-analytics): add @types/jest for formatters test typecheck
chihsuan May 28, 2026
528458b
Merge branch 'trunk' into wooa7s-1320-configure-workspace-and-typescr…
chihsuan Jun 2, 2026
feeb212
docs(premium-analytics): revert internal-packages README section
chihsuan Jun 3, 2026
2beeaa9
fix(premium-analytics): align route name with internal-package conven…
chihsuan Jun 3, 2026
e028a6d
Merge remote-tracking branch 'origin/wooa7s-1320-configure-workspace-…
chihsuan Jun 3, 2026
b33e257
docs(premium-analytics): use canonical package name in formatters README
chihsuan Jun 3, 2026
58e4756
Update projects/packages/premium-analytics/packages/formatters/README.md
chihsuan Jun 3, 2026
b3cbdeb
Update projects/packages/premium-analytics/packages/formatters/README.md
chihsuan Jun 3, 2026
952a10c
Merge remote-tracking branch 'origin/trunk' into wooa7s-1320-configur…
chihsuan Jun 5, 2026
0601ef2
Merge remote-tracking branch 'origin/wooa7s-1320-configure-workspace-…
chihsuan Jun 5, 2026
50d1d3f
Merge remote-tracking branch 'origin/trunk' into wooa7s-1313-integrat…
chihsuan Jun 5, 2026
c52d032
Update projects/packages/premium-analytics/packages/formatters/src/me…
chihsuan Jun 5, 2026
a05f141
Merge remote-tracking branch 'origin/trunk' into wooa7s-1313-integrat…
chihsuan Jun 5, 2026
6f52ebf
Merge branch 'wooa7s-1313-integrate-formatters-package-into-analytics…
chihsuan Jun 5, 2026
d3a8e33
Merge remote-tracking branch 'origin/trunk' into wooa7s-1313-integrat…
chihsuan Jun 5, 2026
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 formatters package (number/currency/percentage metric formatter and date helpers) as an internal package from next-woocommerce-analytics.
37 changes: 26 additions & 11 deletions projects/packages/premium-analytics/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
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).
* Soften JSDoc rules for `packages/datetime/**` and `packages/formatters/**`
* so the initial ports can land with the upstream JSDoc style (descriptions
* on the function body, not on per-param tags). Temporary — backfill proper
* JSDoc on the helpers and remove these overrides (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',
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',
},
},
} );
{
files: [ 'packages/formatters/**' ],
rules: {
'jsdoc/require-description': 'off',
'jsdoc/require-param': '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 @@ -30,6 +30,7 @@
}
},
"dependencies": {
"@automattic/number-formatters": "workspace:*",
"@date-fns/tz": "1.4.1",
"@wordpress/boot": "0.13.0",
"@wordpress/data": "10.46.0",
Expand All @@ -45,6 +46,7 @@
"@babel/core": "7.29.0",
"@storybook/react": "10.3.6",
"@testing-library/dom": "10.4.1",
"@types/jest": "30.0.0",
"@typescript/native-preview": "7.0.0-dev.20260225.1",
"@wordpress/build": "0.14.0",
"@wordpress/ui": "0.13.0",
Expand Down
89 changes: 89 additions & 0 deletions projects/packages/premium-analytics/packages/formatters/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# @automattic/jetpack-premium-analytics-formatters

Locale-aware formatting utilities for Jetpack Premium Analytics.

Thin wrapper over `@automattic/number-formatters` (numbers, currency) and
`date-fns` (dates), plus a domain-specific orchestrator (`formatMetricValue`)
that routes between formatters by analytics metric type.

## Exports

```typescript
import {
formatMetricValue,
formatDate,
formatDateRange,
} from '@jetpack-premium-analytics/formatters';
```

## `formatMetricValue( value, type?, options? )`

Format a numeric value based on its metric type.
Returns `''` for null, undefined, or NaN.

```typescript
formatMetricValue( 9876.543 ); // '9,877'
formatMetricValue( 1500, 'number', {
useMultipliers: true,
decimals: 1,
} ); // '1.5K'
formatMetricValue( 192088.05, 'currency' ); // '$192,088.05'
formatMetricValue( 0.25, 'percentage' ); // '+25%'
formatMetricValue( 4.75, 'average' ); // '4.75'
formatMetricValue( 192088, 'currency', {
useMultipliers: true,
currencyCode: 'EUR',
} ); // '192.09K€'
```

| Parameter | Type | Default | Description |
| ------------------------ | ----------------------------------------------------- | ---------------------------------------- | ---------------------------------------------- |
| `value` | `string \| number \| null` | | Value to format |
| `type` | `'number' \| 'currency' \| 'percentage' \| 'average'` | `'number'` | Formatting strategy |
| `options.decimals` | `number` | varies by type | Decimal precision (0 for number, 2 for others) |
| `options.useMultipliers` | `boolean` | `false` | Compact notation (K/M suffixes) |
| `options.signDisplay` | `Intl` sign mode | `'auto'` (`'exceptZero'` for percentage) | Sign display |
| `options.currencyCode` | `string` | `'USD'` | ISO 4217 currency code |

## `formatDate( date, format? )`

Format a date using a named preset or custom `date-fns` pattern.
Defaults to `'medium'`.

```typescript
formatDate( new Date( '2025-06-21' ) ); // 'Jun 21, 2025'
formatDate( new Date( '2025-06-21' ), 'short' ); // 'Jun 21'
formatDate( new Date( '2025-06-21' ), 'long' ); // 'June 21, 2025'
formatDate( new Date( '2025-06-21' ), 'dd/MM/yyyy' ); // '21/06/2025'
```

**Named presets:** `short`, `medium` (default), `long`, `full`, `day`, `month`, `year`, `monthYear`, `numeric`, `iso`, `dateTime`.

## `formatDateRange( range? )`

Format a date range into a human-readable string.
Returns `''` when range or dates are missing.

```typescript
formatDateRange( { from, to } );
// same day: 'Jun 21, 2025'
// same month: 'Jun 21-25, 2025'
// same year: 'Jun 21-Jul 25, 2025'
// cross-year: 'Jun 21, 2024-Jul 25, 2025'
```

| Parameter | Type | Description |
| --------- | ---------------------------- | ----------------- |
| `range` | `{ from?: Date; to?: Date }` | Date range object |

## Architecture

Number and currency formatting delegates to `@automattic/number-formatters`
(a tier-2 published Jetpack package). Date formatting uses `date-fns`. The
`formatMetricValue` orchestrator is domain-specific — it routes to the right
formatter based on the metric type.

## Dependencies

- `@automattic/number-formatters` — number/currency primitives
- `date-fns` — date formatting
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@automattic/jetpack-premium-analytics-formatters",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"sideEffects": false,
"dependencies": {
"@automattic/number-formatters": "workspace:*",
"date-fns": "4.1.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* Internal dependencies
*/
import { formatDate } from '../format-date';
import { formatDateRange } from '../format-date-range';

jest.mock( '../format-date' );

describe( 'formatDateRange', () => {
/**
* Setup mock for formatDate function.
*/
const setupMocks = () => {
( formatDate as jest.Mock ).mockImplementation( ( date: Date, formatString?: string ) => {
const monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];

const dateStr = date.toISOString().split( 'T' )[ 0 ];

if ( formatString === 'iso' ) {
return dateStr;
}

// eslint-disable-next-line @wordpress/no-unused-vars-before-return -- allow unused vars before return for test mock
const [ year, month, day ] = dateStr.split( '-' );
const monthName = monthNames[ parseInt( month, 10 ) - 1 ];

if ( formatString === 'year' ) {
return year;
}

if ( formatString === 'monthYear' ) {
return `${ monthName } ${ year }`;
}

const dayNum = parseInt( day, 10 );
if ( formatString === 'short' ) {
return `${ monthName } ${ dayNum }`;
}

if ( formatString === 'd, yyyy' ) {
return `${ dayNum }, ${ year }`;
}

// Default: medium format
return `${ monthName } ${ dayNum }, ${ year }`;
} );
};

beforeEach( () => {
jest.clearAllMocks();
setupMocks();
} );

describe( 'edge cases', () => {
it( 'returns empty string when "from" is missing', () => {
const result = formatDateRange( {
from: undefined,
to: new Date( '2025-06-21' ),
} );
expect( result ).toBe( '' );
} );

it( 'returns empty string when "to" is missing', () => {
const result = formatDateRange( {
from: new Date( '2025-06-21' ),
to: undefined,
} );
expect( result ).toBe( '' );
} );

it( 'returns empty string when both dates are missing', () => {
const result = formatDateRange( {
from: undefined,
to: undefined,
} );
expect( result ).toBe( '' );
} );
} );

describe( 'same date', () => {
it( 'formats same date as single date', () => {
const date = new Date( '2025-06-21' );
const result = formatDateRange( { from: date, to: date } );
expect( result ).toBe( 'Jun 21, 2025' );
} );
} );

describe( 'same month and year', () => {
it( 'formats date range within same month', () => {
const from = new Date( '2025-06-21' );
const to = new Date( '2025-06-25' );
const result = formatDateRange( { from, to } );
expect( result ).toBe( 'Jun 21-25, 2025' );
} );
} );

describe( 'same year, different months', () => {
it( 'formats date range across months in same year', () => {
const from = new Date( '2025-06-21' );
const to = new Date( '2025-07-25' );
const result = formatDateRange( { from, to } );
expect( result ).toBe( 'Jun 21-Jul 25, 2025' );
} );
} );

describe( 'different years', () => {
it( 'formats date range across different years', () => {
const from = new Date( '2024-06-21' );
const to = new Date( '2025-07-25' );
const result = formatDateRange( { from, to } );
expect( result ).toBe( 'Jun 21, 2024-Jul 25, 2025' );
} );
} );
} );
Loading
Loading