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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Versions follow [Semantic Versioning](https://semver.org/) — minor bump per su
validated on `REPLACE ... WITH` (rejects malformed or off-granularity values —
no silent coercion), and `LIST STRUCTURE` prints the declared type instead of the
raw SQLite storage class. (#43)
- `WEEK(date)` built-in — ISO-8601 week number (1–53): Monday-start weeks, week 1 is the
week containing the year's first Thursday. Early-January dates correctly report the
previous year's week 52/53, and late-December dates week 1 of the next year. Accepts
ISO `YYYY-MM-DD` or `MM/DD/YY`; invalid input returns 0. (#44)

---

Expand Down
34 changes: 32 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ server/
ServerDatabaseBridge.ts IDatabaseBridge impl wrapping better-sqlite3
ProgramStore.ts .prg program storage in data/system.sqlite3
IndexStore.ts Index metadata + active index in data/system.sqlite3
ColumnMetaStore.ts Declared column types (TIME, TIME(n)) in data/system.sqlite3 — SQLite affinity can't distinguish TIME from CHAR/DATE
ReportStore.ts Report definition storage in data/system.sqlite3 (reports table)
ReportRunner.ts ASCII and HTML report rendering, group breaks, subtotals, grand totals
DemoSeeder.ts Seeds demos/*.prg into the program store and demos/reports/*.json into the report store at startup (demos win)
Expand Down Expand Up @@ -109,6 +110,7 @@ tests/
ServerDatabaseBridge.test.ts
ProgramStore.test.ts
AlterTable.test.ts ALTER TABLE + MODIFY STRUCTURE integration tests
TimeType.test.ts TIME / TIME(n) columns — creation, structure, write validation
Print.test.ts `?` / `??` print command
Aggregate.test.ts `SUM` / `AVERAGE`
Builtins.test.ts / BuiltinsParse.test.ts built-in functions (direct + through the parser)
Expand Down Expand Up @@ -221,6 +223,25 @@ WebBase-III supports **unlimited work areas** (no DOS 10-area limit). Cross-area
| `@ r,c SAY "text" GET <var>` | Define a form field |
| `READ` | Display form and wait for submit |

### Built-in functions

Implemented in `src/interpreter/Builtins.ts` (stateless) and `Executor.ts` (stateful:
`EOF()`, `BOF()`, `FOUND()`, `RECNO()`, `RECCOUNT()`).

> **Adding a built-in:** implementing it in `Builtins.ts` is not enough — it must also be
> added to `BUILTIN_FUNCTIONS` in `src/interpreter/Parser.ts` or the parser rejects the
> call (`Unknown command: (`). This is how the #4 built-ins shipped broken. Always cover a
> new built-in in **both** `tests/Builtins.test.ts` (direct) and `tests/BuiltinsParse.test.ts`
> (through the parser), plus a Playwright case.

Strings: `SUBSTR`, `LEN`, `TRIM`, `LTRIM`, `UPPER`, `LOWER`, `AT`, `STR`, `VAL`, `SPACE`, `REPLICATE`.
Numbers: `INT`, `ABS`, `ROUND`, `MOD`, `MAX`, `MIN`.
Dates/times: `DATE()`, `TIME()`, `DTOC`, `CTOD`, `YEAR`, `MONTH`, `DAY`, `WEEK`.

| Function | What it returns |
|---|---|
| `WEEK(date)` | ISO-8601 week number (1–53). Monday-start weeks; week 1 is the week containing the year's first Thursday, so early-January dates can return 52/53 (belonging to the previous year's last week) and late-December dates can return 1. Accepts ISO `YYYY-MM-DD` or `MM/DD/YY`; invalid input → 0. |

### Control flow
| Command | What it does |
|---|---|
Expand Down Expand Up @@ -254,6 +275,7 @@ line.
1. ~~Indexing & Search~~ — `INDEX ON`, `SET INDEX TO`, `SEEK`, `FIND`, `REINDEX`, `LIST INDEXES` ✅
2. ~~Language Completeness~~ — `DO CASE/ENDCASE`, built-in functions (`EOF()`, `BOF()`, `FOUND()`, `RECNO()`, `RECCOUNT()`, `SUBSTR()`, `STR()`, `AT()`, `UPPER()`, `LOWER()`, `ROUND()`, `MOD()`, `MAX()`, `MIN()`, `TIME()`, `YEAR()`, `MONTH()`, `DAY()`, and more) ✅
- `ROUND`/`MOD`/`MAX`/`MIN`/`TIME`/`YEAR`/`MONTH`/`DAY` contributed by [@kas2804](https://github.com/kas2804) in PR #17 (#4). 🙏
- `WEEK()` added in v1.2.0 (#44).
3. ~~Multi-Work-Area~~ — unlimited `SELECT <alias>`, `SET RELATION TO`, `alias.field` notation ✅
4. ~~Report & Label Engine~~ — `REPORT FORM`, group breaks, subtotals, HTML preview ✅
5. ~~The Assistant~~ — sidebar GUI, wizards, catalog protocol ✅
Expand All @@ -266,18 +288,26 @@ line.
so other sessions BROWSE-ing that table refresh automatically (#11) ✅
- ~~JOIN to materialize a combined table~~ — `JOIN WITH <alias> TO <file> FOR <cond> [FIELDS <list>]`, snapshot table via SQLite join (#10) ✅

### Beyond parity (v1.2.0 — in progress)

- ~~`TIME` column type~~ — `TIME`/`TIME(n)` columns storing `HH:MM`, with a minute-granularity
qualifier validated on write; declared types tracked in `server/ColumnMetaStore.ts` (#43) ✅
- ~~`WEEK()` built-in~~ — ISO-8601 week number (#44) ✅
- BROWSE per-cell validation — grid rejects invalid edits per column type (#45)
- `demos/overtime.prg` — overtime tracker showcasing all three of the above (#46)

## Boolean literals

Both styles accepted: `TRUE`/`FALSE` and `.T.`/`.TRUE.`/`.F.`/`.FALSE.` (dBASE III style). Output always uses `.T.`/`.F.`. Logical operators likewise: `NOT`/`.NOT.`, `AND`/`.AND.`, `OR`/`.OR.`.

## Testing

```bash
npm test # Vitest unit + integration (265 tests)
npm test # Vitest unit + integration (281 tests)
npx playwright test # E2E browser tests — requires dev server on :5173/:3000
```

Playwright suites (73 tests): `tests/integration.spec.ts` (20 tests — full REPL scenario), `tests/assistant.spec.ts` (20 tests — sidebar, wizards, report designer, MODIFY STRUCTURE round-trip, program run, CSV/SORT/SUM-AVERAGE/REINDEX/PACK actions, demo launchers), `tests/inventory.spec.ts` (8 tests — INVENTORY.prg menu + valuation/low-stock report/sort/CSV/JOIN), `tests/crm.spec.ts` (6 tests — CRM demo menu, pipeline summary, sort, report, CSV, JOIN), `tests/multiarea.spec.ts` (4 tests — multi-work-area, relations, alias.field), `tests/parity-commands.spec.ts` (4 tests — `?`/`??`, built-in functions, `SUM`/`AVERAGE`, `SORT ON … TO`), `tests/demos.spec.ts` (4 tests — demo program + report seeding), `tests/copycsv.spec.ts` (2 tests — COPY TO download + APPEND FROM upload), `tests/splash.spec.ts` (2 tests — version banner + demo discoverability), `tests/join.spec.ts` (1 test — JOIN materialization), `tests/propagation.spec.ts` (1 test — live multiuser refresh), `tests/program-side-effects.spec.ts` (1 test — CSV/report side-effects fire from inside a program block).
Playwright suites (75 tests): `tests/integration.spec.ts` (20 tests — full REPL scenario), `tests/assistant.spec.ts` (21 tests — sidebar, wizards, report designer, MODIFY STRUCTURE round-trip, `TIME(15)` column + REPLACE validation, program run, CSV/SORT/SUM-AVERAGE/REINDEX/PACK actions, demo launchers), `tests/inventory.spec.ts` (8 tests — INVENTORY.prg menu + valuation/low-stock report/sort/CSV/JOIN), `tests/crm.spec.ts` (6 tests — CRM demo menu, pipeline summary, sort, report, CSV, JOIN), `tests/parity-commands.spec.ts` (5 tests — `?`/`??`, built-in functions, `WEEK()`, `SUM`/`AVERAGE`, `SORT ON … TO`), `tests/multiarea.spec.ts` (4 tests — multi-work-area, relations, alias.field), `tests/demos.spec.ts` (4 tests — demo program + report seeding), `tests/copycsv.spec.ts` (2 tests — COPY TO download + APPEND FROM upload), `tests/splash.spec.ts` (2 tests — version banner + demo discoverability), `tests/join.spec.ts` (1 test — JOIN materialization), `tests/propagation.spec.ts` (1 test — live multiuser refresh), `tests/program-side-effects.spec.ts` (1 test — CSV/report side-effects fire from inside a program block).

## Definition of done

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ Functions work anywhere an expression is accepted — `IF`, `DO WHILE`, `STORE`,
| `YEAR(date)` | Numeric year from ISO date string |
| `MONTH(date)` | Numeric month from ISO date string |
| `DAY(date)` | Numeric day from ISO date string |
| `WEEK(date)` | ISO-8601 week number (1–53) — Monday-start weeks, week 1 holds the year's first Thursday |

### Boolean literals

Expand Down
25 changes: 25 additions & 0 deletions src/interpreter/Builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,31 @@ export function callStateless(fn: string, args: unknown[]): unknown {
const d = new Date(s(0));
return isNaN(d.getTime()) ? 0 : d.getDate();
}
case 'WEEK': {
// ISO-8601 week number: Monday-start weeks, week 1 holds the year's first
// Thursday. Dates in early January can therefore belong to week 52/53 of
// the previous year, and late December to week 1 of the next.
const raw = s(0);
const iso = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/);
let y: number, m: number, day: number;
if (iso) {
y = Number(iso[1]); m = Number(iso[2]); day = Number(iso[3]);
} else {
const d = new Date(raw);
if (isNaN(d.getTime())) return 0;
y = d.getFullYear(); m = d.getMonth() + 1; day = d.getDate();
}
// Work in UTC so no local timezone offset can shift the day.
const dt = new Date(Date.UTC(y, m - 1, day));
if (isNaN(dt.getTime())) return 0;
// Date.UTC rolls impossible dates over (Feb 30 → Mar 1), so reject any
// input the round-trip doesn't reproduce exactly.
if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== m - 1 || dt.getUTCDate() !== day) return 0;
const dow = dt.getUTCDay() || 7; // Mon=1 … Sun=7
dt.setUTCDate(dt.getUTCDate() + 4 - dow); // Thursday fixes the week's year
const yearStart = Date.UTC(dt.getUTCFullYear(), 0, 1);
return Math.ceil(((dt.getTime() - yearStart) / 86400000 + 1) / 7);
}
default:
throw new Error(`Unknown function: ${fn}`);
}
Expand Down
1 change: 1 addition & 0 deletions src/interpreter/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const BUILTIN_FUNCTIONS = new Set([
// #4 (PR #17, @kas2804) — implemented in Builtins.ts; must be whitelisted here
// too or the parser won't recognise the call.
'ROUND','MOD','MAX','MIN','TIME','YEAR','MONTH','DAY',
'WEEK',
]);

// ── AST Node Types ──────────────────────────────────────────────────────────
Expand Down
30 changes: 30 additions & 0 deletions tests/Builtins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,36 @@ describe('DAY', () => {
it('invalid date returns 0', () => expect(callStateless('DAY', ['not-a-date'])).toBe(0));
});

describe('WEEK', () => {
it('returns the ISO week number of a mid-year date', () => expect(callStateless('WEEK', ['2024-05-12'])).toBe(19));
it('week 1 starts on the Monday of the week holding the first Thursday', () => {
expect(callStateless('WEEK', ['2024-01-01'])).toBe(1); // Monday, Jan 1
expect(callStateless('WEEK', ['2026-01-01'])).toBe(1); // Thursday, Jan 1
});
it('early-January dates can belong to the last week of the previous year', () => {
expect(callStateless('WEEK', ['2021-01-01'])).toBe(53); // Friday → week 53 of 2020
expect(callStateless('WEEK', ['2022-01-01'])).toBe(52); // Saturday → week 52 of 2021
expect(callStateless('WEEK', ['2023-01-01'])).toBe(52); // Sunday → week 52 of 2022
});
it('late-December dates can belong to week 1 of the next year', () => {
expect(callStateless('WEEK', ['2024-12-30'])).toBe(1); // Monday → week 1 of 2025
expect(callStateless('WEEK', ['2019-12-30'])).toBe(1); // Monday → week 1 of 2020
});
it('handles 53-week years', () => {
expect(callStateless('WEEK', ['2020-12-31'])).toBe(53);
expect(callStateless('WEEK', ['2026-12-31'])).toBe(53);
expect(callStateless('WEEK', ['2016-01-03'])).toBe(53); // Sunday → week 53 of 2015
});
it('accepts MM/DD/YY display dates', () => expect(callStateless('WEEK', ['05/12/24'])).toBe(19));
it('invalid date returns 0', () => expect(callStateless('WEEK', ['not-a-date'])).toBe(0));
it('ISO-shaped but impossible dates return 0 rather than rolling over', () => {
expect(callStateless('WEEK', ['2024-13-45'])).toBe(0); // month 13, day 45
expect(callStateless('WEEK', ['2024-02-30'])).toBe(0); // Feb 30 never exists
expect(callStateless('WEEK', ['2023-02-29'])).toBe(0); // 2023 is not a leap year
});
it('accepts a real leap day', () => expect(callStateless('WEEK', ['2024-02-29'])).toBe(9));
});

describe('unknown function', () => {
it('throws', () => expect(() => callStateless('FOOBAR', [])).toThrow('Unknown function: FOOBAR'));
});
2 changes: 2 additions & 0 deletions tests/BuiltinsParse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ describe('built-in functions reachable through the REPL parser', () => {
it('YEAR', async () => { expect(await evalPrint('YEAR(CTOD("12/25/2026"))')).toContain('2026'); });
it('MONTH', async () => { expect(await evalPrint('MONTH(CTOD("12/25/2026"))')).toContain('12'); });
it('DAY', async () => { expect(await evalPrint('DAY(CTOD("12/25/2026"))')).toContain('25'); });
it('WEEK', async () => { expect(await evalPrint('WEEK("2024-05-12")')).toContain('19'); });
it('WEEK across a year boundary', async () => { expect(await evalPrint('WEEK("2021-01-01")')).toContain('53'); });
});
29 changes: 29 additions & 0 deletions tests/parity-commands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ async function boot(page: Page, dbName: string): Promise<void> {
await waitForOutput(page, 'Opened database', 3000);
}

// Run `? <expr>` and return the printed value — the last rendered output line,
// which is the result rather than the echoed command.
async function printResult(page: Page, expr: string): Promise<string> {
await cmd(page, `? ${expr}`);
const text = await page.locator('#terminal-output .t-line').last().textContent() ?? '';
return text.trim();
}

test.describe('Parity commands e2e', () => {

test('1. ? / ?? print expressions', async ({ page }) => {
Expand Down Expand Up @@ -63,6 +71,27 @@ test.describe('Parity commands e2e', () => {
await expect(page.locator('#terminal-output')).toContainText(/\d\d:\d\d:\d\d/, { timeout: 3000 });
});

// #44 — WEEK() ISO-8601 week number, asserted on the printed value (not just
// "the digits appear somewhere in the scrollback").
test('2b. WEEK() built-in via ?', async ({ page }) => {
await boot(page, `e2e_parity_week_${Date.now()}`);

expect(await printResult(page, 'WEEK("2024-05-12")')).toBe('19');

// Week 1 is the week holding the year's first Thursday.
expect(await printResult(page, 'WEEK("2026-01-01")')).toBe('1');

// Early January can fall in the previous year's last week …
expect(await printResult(page, 'WEEK("2021-01-01")')).toBe('53');
expect(await printResult(page, 'WEEK("2023-01-01")')).toBe('52');

// … and late December in week 1 of the next.
expect(await printResult(page, 'WEEK("2024-12-30")')).toBe('1');

// Composes with CTOD, like the other date built-ins.
expect(await printResult(page, 'WEEK(CTOD("05/12/24"))')).toBe('19');
});

test('3. SUM and AVERAGE', async ({ page }) => {
const db = `e2e_parity_sum_${Date.now()}`;
await boot(page, db);
Expand Down
Loading