Skip to content

Commit 7644726

Browse files
committed
Add support for parsing negative numbers in OSF files
## Summary - Introduced functionality to handle negative numeric literals in the parser. - Updated the `parseNumber` function to correctly interpret negative signs and validate number formats. - Added comprehensive test cases to ensure accurate parsing of negative numbers in various contexts, including arrays, nested objects, and sheet data. - Implemented error handling for invalid negative number formats. ## Testing - Run `pnpm test` to verify all new and existing tests pass successfully.
1 parent 51b402a commit 7644726

File tree

3 files changed

+177
-0
lines changed

3 files changed

+177
-0
lines changed

examples/negative_numbers.osf

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
@meta {
2+
title: "Negative Numbers Example";
3+
author: "OSF Parser";
4+
version: "0.5.0";
5+
temperature: -25;
6+
balance: -1000.50;
7+
coordinates: [-10, -5.5, 0, 5.5, 10];
8+
}
9+
10+
@sheet {
11+
name: "Financial Data";
12+
cols: [Account, Balance, Change];
13+
data {
14+
(1,1) = "Checking";
15+
(1,2) = -500.25;
16+
(1,3) = -50.00;
17+
(2,1) = "Savings";
18+
(2,2) = 1000.00;
19+
(2,3) = -25.50;
20+
(3,1) = "Credit";
21+
(3,2) = -2500.75;
22+
(3,3) = -100.00;
23+
}
24+
formula (4,2): "=B1+B2+B3";
25+
formula (4,3): "=C1+C2+C3";
26+
}
27+
28+
@slide {
29+
title: "Temperature Data";
30+
layout: TitleAndBullets;
31+
bullets {
32+
"Minimum temperature: -40°C";
33+
"Average winter temp: -15.5°C";
34+
"Freezing point: 0°C";
35+
"Room temperature: 22°C";
36+
}
37+
metadata: {
38+
minTemp: -40;
39+
maxTemp: 45;
40+
range: 85;
41+
measurements: [-40, -30, -20, -10, 0, 10, 20, 30, 40];
42+
};
43+
}

parser/src/parser.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,20 @@ function parseString(str: string, i: number): { value: string; index: number } {
9090

9191
function parseNumber(str: string, i: number): { value: number; index: number } {
9292
let j = i;
93+
94+
// Handle optional negative sign
95+
if (j < str.length && str[j] === '-') {
96+
j++;
97+
}
98+
99+
// Parse digits and decimal point
93100
while (j < str.length && /[0-9.]/.test(str[j]!)) j++;
101+
102+
// Ensure we actually parsed some digits after the optional minus sign
103+
if (j === i || (j === i + 1 && str[i] === '-')) {
104+
throw new Error('Invalid number format');
105+
}
106+
94107
return { value: Number(str.slice(i, j)), index: j };
95108
}
96109

@@ -120,6 +133,7 @@ function parseValue(str: string, i: number): { value: any; index: number } {
120133
return { value: res.obj, index: res.index + 1 };
121134
}
122135
if (/\d/.test(ch)) return parseNumber(str, i);
136+
if (ch === '-' && i + 1 < str.length && /\d/.test(str[i + 1]!)) return parseNumber(str, i);
123137
if (str.startsWith('true', i)) return { value: true, index: i + 4 };
124138
if (str.startsWith('false', i)) return { value: false, index: i + 5 };
125139
const id = parseIdentifier(str, i);

parser/tests/parser.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,126 @@ describe('OSF Parser', () => {
283283
expect(docBlock.content).toBe('');
284284
});
285285

286+
it('should parse negative numeric literals', () => {
287+
const input = `@meta {
288+
temperature: -25;
289+
balance: -1000.50;
290+
offset: -0.5;
291+
zero: 0;
292+
positive: 42;
293+
}`;
294+
295+
const result = parse(input);
296+
297+
expect(result.blocks).toHaveLength(1);
298+
expect(result.blocks[0]?.type).toBe('meta');
299+
300+
const metaBlock = result.blocks[0] as MetaBlock;
301+
expect(metaBlock.props.temperature).toBe(-25);
302+
expect(metaBlock.props.balance).toBe(-1000.50);
303+
expect(metaBlock.props.offset).toBe(-0.5);
304+
expect(metaBlock.props.zero).toBe(0);
305+
expect(metaBlock.props.positive).toBe(42);
306+
});
307+
308+
it('should parse negative numbers in arrays', () => {
309+
const input = `@meta {
310+
values: [-10, -5.5, 0, 5.5, 10];
311+
coordinates: [-1, -2, -3];
312+
}`;
313+
314+
const result = parse(input);
315+
const metaBlock = result.blocks[0] as MetaBlock;
316+
317+
expect(metaBlock.props.values).toEqual([-10, -5.5, 0, 5.5, 10]);
318+
expect(metaBlock.props.coordinates).toEqual([-1, -2, -3]);
319+
});
320+
321+
it('should parse negative numbers in nested objects', () => {
322+
const input = `@meta {
323+
position: { x: -10; y: -20.5; z: -0.1; };
324+
range: { min: -100; max: 100; };
325+
}`;
326+
327+
const result = parse(input);
328+
const metaBlock = result.blocks[0] as MetaBlock;
329+
330+
expect(metaBlock.props.position).toEqual({ x: -10, y: -20.5, z: -0.1 });
331+
expect(metaBlock.props.range).toEqual({ min: -100, max: 100 });
332+
});
333+
334+
it('should parse negative numbers in sheet data', () => {
335+
const input = `@sheet {
336+
name: "NegativeNumbers";
337+
data {
338+
(1,1) = -42;
339+
(1,2) = -3.14159;
340+
(2,1) = -0.001;
341+
(2,2) = 100;
342+
}
343+
}`;
344+
345+
const result = parse(input);
346+
const sheetBlock = result.blocks[0] as SheetBlock;
347+
348+
expect(sheetBlock.data).toEqual({
349+
'1,1': -42,
350+
'1,2': -3.14159,
351+
'2,1': -0.001,
352+
'2,2': 100
353+
});
354+
});
355+
356+
it('should handle edge cases for negative numbers', () => {
357+
const input = `@meta {
358+
minusZero: -0;
359+
largeNegative: -999999999;
360+
smallNegative: -0.000001;
361+
}`;
362+
363+
const result = parse(input);
364+
const metaBlock = result.blocks[0] as MetaBlock;
365+
366+
expect(metaBlock.props.minusZero).toBe(-0);
367+
expect(metaBlock.props.largeNegative).toBe(-999999999);
368+
expect(metaBlock.props.smallNegative).toBe(-0.000001);
369+
});
370+
371+
it('should throw error for invalid negative number formats', () => {
372+
const invalidInputs = [
373+
`@meta { invalid: -; }`,
374+
`@meta { invalid: -abc; }`,
375+
`@meta { invalid: --5; }`,
376+
];
377+
378+
invalidInputs.forEach(input => {
379+
expect(() => parse(input)).toThrow();
380+
});
381+
});
382+
383+
it('should serialize negative numbers correctly', () => {
384+
const doc: OSFDocument = {
385+
blocks: [
386+
{
387+
type: 'meta',
388+
props: {
389+
negative: -42,
390+
decimal: -3.14,
391+
zero: -0,
392+
positive: 100
393+
}
394+
} as MetaBlock
395+
]
396+
};
397+
398+
const serialized = serialize(doc);
399+
400+
expect(serialized).toContain('negative: -42');
401+
expect(serialized).toContain('decimal: -3.14');
402+
expect(serialized).toContain('zero: 0'); // -0 should serialize as 0
403+
expect(serialized).toContain('positive: 100');
404+
});
405+
286406
});
287407

288408
describe('serialize', () => {

0 commit comments

Comments
 (0)