diff --git a/SKILL.md b/SKILL.md index 8fcced24..c0cf6c2f 100644 --- a/SKILL.md +++ b/SKILL.md @@ -539,7 +539,7 @@ cli({ | `select` | Extract JSON path | `select: data.items` | | `map` | Map fields | `map: { title: "${{ item.title }}" }` | | `filter` | Filter items | `filter: item.score > 100` | -| `sort` | Sort items | `sort: { by: score, order: desc }` | +| `sort` | Sort items (`numeric: true` for number strings) | `sort: { by: score, order: desc }` | | `limit` | Cap result count | `limit: ${{ args.limit }}` | | `intercept` | Declarative XHR capture | `intercept: { trigger: "navigate:...", capture: "api/hot" }` | | `tap` | Store action + XHR capture | `tap: { store: "feed", action: "fetchFeeds", capture: "homefeed" }` | diff --git a/src/pipeline/steps/transform.ts b/src/pipeline/steps/transform.ts index 1b034c17..4ea0c002 100644 --- a/src/pipeline/steps/transform.ts +++ b/src/pipeline/steps/transform.ts @@ -59,11 +59,15 @@ export async function stepSort(_page: IPage | null, params: unknown, data: unkno if (!Array.isArray(data)) return data; const key = isRecord(params) ? String(params.by ?? '') : String(params); const reverse = isRecord(params) ? params.order === 'desc' : false; + // numeric: true -- convert values with Number() before comparison (handles string-encoded numbers) + const numeric = isRecord(params) ? params.numeric === true : false; return [...data].sort((a, b) => { const left = isRecord(a) ? a[key] : undefined; const right = isRecord(b) ? b[key] : undefined; - const va = left ?? ''; - const vb = right ?? ''; + const na = Number(left); + const nb = Number(right); + const va = numeric ? (Number.isFinite(na) ? na : 0) : (left ?? ''); + const vb = numeric ? (Number.isFinite(nb) ? nb : 0) : (right ?? ''); const cmp = va < vb ? -1 : va > vb ? 1 : 0; return reverse ? -cmp : cmp; }); diff --git a/src/pipeline/transform.test.ts b/src/pipeline/transform.test.ts index 7e00157b..527f95a4 100644 --- a/src/pipeline/transform.test.ts +++ b/src/pipeline/transform.test.ts @@ -101,6 +101,57 @@ describe('stepSort', () => { await stepSort(null, 'score', SAMPLE_DATA, {}); expect(SAMPLE_DATA).toEqual(original); }); + + it('sorts string-encoded numbers numerically with numeric: true', async () => { + const data = [ + { name: 'A', volume: '99' }, + { name: 'B', volume: '1000' }, + { name: 'C', volume: '250' }, + ]; + const result = await stepSort(null, { by: 'volume', order: 'desc', numeric: true }, data, {}); + expect((result as typeof data).map((r) => r.name)).toEqual(['B', 'C', 'A']); + }); + + it('sorts string-encoded numbers lexicographically without numeric', async () => { + const data = [ + { name: 'A', volume: '99' }, + { name: 'B', volume: '1000' }, + { name: 'C', volume: '250' }, + ]; + const result = await stepSort(null, { by: 'volume', order: 'desc' }, data, {}); + // Lexicographic: "99" > "250" > "1000" + expect((result as typeof data).map((r) => r.name)).toEqual(['A', 'C', 'B']); + }); + + it('treats non-numeric values as 0 when numeric: true', async () => { + const data = [ + { name: 'A', value: '100' }, + { name: 'B', value: 'N/A' }, + { name: 'C', value: '50' }, + ]; + const result = await stepSort(null, { by: 'value', order: 'asc', numeric: true }, data, {}); + expect((result as typeof data).map((r) => r.name)).toEqual(['B', 'C', 'A']); + }); + + it('handles "0" and negative numbers correctly with numeric: true', async () => { + const data = [ + { name: 'A', value: '-10' }, + { name: 'B', value: '0' }, + { name: 'C', value: '5' }, + ]; + const result = await stepSort(null, { by: 'value', order: 'asc', numeric: true }, data, {}); + expect((result as typeof data).map((r) => r.name)).toEqual(['A', 'B', 'C']); + }); + + it('treats missing fields as 0 when numeric: true', async () => { + const data = [ + { name: 'A', value: '10' }, + { name: 'B' }, + { name: 'C', value: '-5' }, + ]; + const result = await stepSort(null, { by: 'value', order: 'asc', numeric: true }, data, {}); + expect((result as typeof data).map((r) => r.name)).toEqual(['C', 'B', 'A']); + }); }); describe('stepLimit', () => {