Skip to content

Commit 6f3ddd7

Browse files
committed
Initial implementation for the store
1 parent fbeb3c5 commit 6f3ddd7

File tree

5 files changed

+324
-0
lines changed

5 files changed

+324
-0
lines changed

src/google/utils/date.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Returns the current time in milliseconds since Unix epoch
3+
* @returns number - Current timestamp in milliseconds
4+
*/
5+
export function getCurrentTimeMs(): number {
6+
return Date.now();
7+
}

src/google/utils/range.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Combines sheet name and range into A1 notation
3+
*
4+
* @param sheetName - Name of the sheet
5+
* @param range - Range in A1 notation without sheet name
6+
* @returns Complete A1 notation with sheet name
7+
*/
8+
export function getA1Range(sheetName: string, range: string): string {
9+
return `${sheetName}!${range}`;
10+
}
11+
12+
/**
13+
* Represents a column index with its A1 notation name and numeric index
14+
*/
15+
export interface ColIdx {
16+
name: string;
17+
idx: number;
18+
}
19+
20+
/**
21+
* Maps column identifiers to their A1 notation and index information
22+
*/
23+
export class ColsMapping {
24+
private mapping: Map<string, ColIdx>;
25+
26+
constructor(mapping: Map<string, ColIdx>) {
27+
this.mapping = mapping;
28+
}
29+
30+
public getNameMap(): Map<string, string> {
31+
const entries = this.mapping.entries();
32+
return new Map<string, string>(
33+
Array.from(entries).map(([key, value]) => [key, value.name])
34+
);
35+
}
36+
}
37+
38+
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
39+
40+
/**
41+
* Generates A1 notation column mapping for given column identifiers
42+
*
43+
* @param columns - Array of column identifiers
44+
* @returns Mapping of column identifiers to their A1 notation and index
45+
*/
46+
export function generateColumnMapping(columns: string[]): Map<string, ColIdx> {
47+
return new Map(
48+
columns.map((col, idx) => [
49+
col,
50+
{
51+
name: generateColumnName(idx),
52+
idx,
53+
},
54+
])
55+
);
56+
}
57+
58+
/**
59+
* Converts a zero-based index to A1 notation column name
60+
*
61+
* @param n - Zero-based column index
62+
* @returns Column name in A1 notation (e.g., A, B, ..., Z, AA, AB, etc.)
63+
*
64+
* @example
65+
* ```typescript
66+
* generateColumnName(0) // returns "A"
67+
* generateColumnName(25) // returns "Z"
68+
* generateColumnName(26) // returns "AA"
69+
* generateColumnName(27) // returns "AB"
70+
* ```
71+
*/
72+
export function generateColumnName(n: number): string {
73+
// This is not a pure Base26 conversion since the second char can start from "A" (or 0) again.
74+
// In a normal Base26 int to string conversion, the second char can only start from "B" (or 1).
75+
// Hence, we need to handle the first digit separately from subsequent digits.
76+
// For subsequent digits, we subtract 1 first to ensure they start from 0, not 1.
77+
let col = ALPHABET[n % 26]!;
78+
n = Math.floor(n / 26);
79+
80+
while (n > 0) {
81+
n -= 1;
82+
col = ALPHABET[n % 26]! + col;
83+
n = Math.floor(n / 26);
84+
}
85+
86+
return col;
87+
}

src/google/utils/values.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Escapes a value to ensure proper string representation in Google Sheets.
3+
* Prefixes strings with a single quote to prevent automatic type conversion.
4+
*
5+
* @param value - The value to escape
6+
* @returns The escaped value
7+
*/
8+
export function escapeValue(value: any): any {
9+
if (typeof value === 'string') {
10+
return `'${value}`;
11+
}
12+
return value;
13+
}
14+
15+
/**
16+
* Error class for IEEE 754 safe integer boundary violations
17+
*/
18+
export class IEEE754SafeIntegerError extends Error {
19+
constructor() {
20+
super('Integer provided is not within the IEEE 754 safe integer boundary of [-(2^53), 2^53], the integer may have a precision loss');
21+
this.name = 'IEEE754SafeIntegerError';
22+
}
23+
}
24+
25+
/**
26+
* Checks if a numeric value is within IEEE 754 safe integer boundaries.
27+
*
28+
* @param value - The value to check
29+
* @throws {IEEE754SafeIntegerError} If the value is outside safe integer boundaries
30+
*/
31+
export function checkIEEE754SafeInteger(value: any): void {
32+
if (typeof value !== 'number') {
33+
return;
34+
}
35+
36+
if (!Number.isSafeInteger(value)) {
37+
throw new IEEE754SafeIntegerError();
38+
}
39+
}

tests/google/utils/range.test.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import {
2+
getA1Range,
3+
generateColumnName,
4+
generateColumnMapping,
5+
ColIdx,
6+
} from '../../../src/google/utils/range';
7+
8+
describe('range', () => {
9+
describe('getA1Range', () => {
10+
it('should correctly combine sheet name and range', () => {
11+
expect(getA1Range('sheet', 'A1:A50')).toBe('sheet!A1:A50');
12+
expect(getA1Range('sheet', 'A1')).toBe('sheet!A1');
13+
expect(getA1Range('sheet', 'A')).toBe('sheet!A');
14+
});
15+
});
16+
17+
describe('generateColumnName', () => {
18+
interface TestCase {
19+
name: string;
20+
input: number;
21+
expected: string;
22+
}
23+
24+
const testCases: TestCase[] = [
25+
{
26+
name: 'zero',
27+
input: 0,
28+
expected: 'A',
29+
},
30+
{
31+
name: 'single_character',
32+
input: 15,
33+
expected: 'P',
34+
},
35+
{
36+
name: 'single_character_2',
37+
input: 25,
38+
expected: 'Z',
39+
},
40+
{
41+
name: 'single_character_3',
42+
input: 5,
43+
expected: 'F',
44+
},
45+
{
46+
name: 'double_character',
47+
input: 26,
48+
expected: 'AA',
49+
},
50+
{
51+
name: 'double_character_2',
52+
input: 52,
53+
expected: 'BA',
54+
},
55+
{
56+
name: 'double_character_3',
57+
input: 89,
58+
expected: 'CL',
59+
},
60+
{
61+
name: 'max_column',
62+
input: 18277,
63+
expected: 'ZZZ',
64+
},
65+
];
66+
67+
testCases.forEach(tc => {
68+
it(tc.name, () => {
69+
expect(generateColumnName(tc.input)).toBe(tc.expected);
70+
});
71+
});
72+
});
73+
74+
describe('generateColumnMapping', () => {
75+
interface TestCase {
76+
name: string;
77+
input: string[];
78+
expected: Map<string, ColIdx>;
79+
}
80+
81+
const testCases: TestCase[] = [
82+
{
83+
name: 'single_column',
84+
input: ['col1'],
85+
expected: new Map([
86+
['col1', { name: 'A', idx: 0 }],
87+
]),
88+
},
89+
{
90+
name: 'three_column',
91+
input: ['col1', 'col2', 'col3'],
92+
expected: new Map([
93+
['col1', { name: 'A', idx: 0 }],
94+
['col2', { name: 'B', idx: 1 }],
95+
['col3', { name: 'C', idx: 2 }],
96+
]),
97+
},
98+
{
99+
name: 'many_column',
100+
input: [
101+
'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10',
102+
'c11', 'c12', 'c13', 'c14', 'c15', 'c16', 'c17', 'c18', 'c19', 'c20',
103+
'c21', 'c22', 'c23', 'c24', 'c25', 'c26', 'c27', 'c28',
104+
],
105+
expected: new Map([
106+
['c1', { name: 'A', idx: 0 }], ['c2', { name: 'B', idx: 1 }],
107+
['c3', { name: 'C', idx: 2 }], ['c4', { name: 'D', idx: 3 }],
108+
['c5', { name: 'E', idx: 4 }], ['c6', { name: 'F', idx: 5 }],
109+
['c7', { name: 'G', idx: 6 }], ['c8', { name: 'H', idx: 7 }],
110+
['c9', { name: 'I', idx: 8 }], ['c10', { name: 'J', idx: 9 }],
111+
['c11', { name: 'K', idx: 10 }], ['c12', { name: 'L', idx: 11 }],
112+
['c13', { name: 'M', idx: 12 }], ['c14', { name: 'N', idx: 13 }],
113+
['c15', { name: 'O', idx: 14 }], ['c16', { name: 'P', idx: 15 }],
114+
['c17', { name: 'Q', idx: 16 }], ['c18', { name: 'R', idx: 17 }],
115+
['c19', { name: 'S', idx: 18 }], ['c20', { name: 'T', idx: 19 }],
116+
['c21', { name: 'U', idx: 20 }], ['c22', { name: 'V', idx: 21 }],
117+
['c23', { name: 'W', idx: 22 }], ['c24', { name: 'X', idx: 23 }],
118+
['c25', { name: 'Y', idx: 24 }], ['c26', { name: 'Z', idx: 25 }],
119+
['c27', { name: 'AA', idx: 26 }], ['c28', { name: 'AB', idx: 27 }],
120+
]),
121+
},
122+
];
123+
124+
testCases.forEach(tc => {
125+
it(tc.name, () => {
126+
expect(generateColumnMapping(tc.input)).toEqual(tc.expected);
127+
});
128+
});
129+
});
130+
});

tests/google/utils/values.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
escapeValue,
3+
checkIEEE754SafeInteger,
4+
IEEE754SafeIntegerError,
5+
} from '../../../src/google/utils/values';
6+
7+
describe('values', () => {
8+
describe('escapeValue', () => {
9+
it('should escape string values with a leading quote', () => {
10+
expect(escapeValue('blah')).toBe("'blah");
11+
});
12+
13+
it('should not modify non-string values', () => {
14+
expect(escapeValue(1)).toBe(1);
15+
expect(escapeValue(true)).toBe(true);
16+
expect(escapeValue(null)).toBe(null);
17+
expect(escapeValue(undefined)).toBe(undefined);
18+
expect(escapeValue({ key: 'value' })).toEqual({ key: 'value' });
19+
});
20+
});
21+
22+
describe('checkIEEE754SafeInteger', () => {
23+
it('should accept zero as a safe integer', () => {
24+
expect(() => checkIEEE754SafeInteger(0)).not.toThrow();
25+
});
26+
27+
it('should accept integers within safe bounds', () => {
28+
// Test lower bound: -(2^53)
29+
expect(() => checkIEEE754SafeInteger(-9007199254740991)).not.toThrow();
30+
31+
// Test upper bound: 2^53
32+
expect(() => checkIEEE754SafeInteger(9007199254740991)).not.toThrow();
33+
34+
// Test some regular integers
35+
expect(() => checkIEEE754SafeInteger(42)).not.toThrow();
36+
expect(() => checkIEEE754SafeInteger(-42)).not.toThrow();
37+
});
38+
39+
it('should reject integers outside safe bounds', () => {
40+
// Test below lower bound: -(2^53) - 1
41+
expect(() => checkIEEE754SafeInteger(-9007199254740993)).toThrow(IEEE754SafeIntegerError);
42+
43+
// Test above upper bound: (2^53) + 1
44+
expect(() => checkIEEE754SafeInteger(9007199254740993)).toThrow(IEEE754SafeIntegerError);
45+
});
46+
47+
it('should ignore non-numeric values', () => {
48+
expect(() => checkIEEE754SafeInteger('blah')).not.toThrow();
49+
expect(() => checkIEEE754SafeInteger(true)).not.toThrow();
50+
expect(() => checkIEEE754SafeInteger([])).not.toThrow();
51+
expect(() => checkIEEE754SafeInteger({})).not.toThrow();
52+
expect(() => checkIEEE754SafeInteger(null)).not.toThrow();
53+
expect(() => checkIEEE754SafeInteger(undefined)).not.toThrow();
54+
});
55+
56+
it('should reject non-integer numbers', () => {
57+
expect(() => checkIEEE754SafeInteger(3.14)).toThrow(IEEE754SafeIntegerError);
58+
expect(() => checkIEEE754SafeInteger(-2.5)).toThrow(IEEE754SafeIntegerError);
59+
});
60+
});
61+
});

0 commit comments

Comments
 (0)