Skip to content
Open
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
51 changes: 51 additions & 0 deletions lib/quoteRequiringCharacters.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, test, expect } from 'vitest';
import { reRefusedAbove00AF } from './quoteRequiringCharacters.ts';

/** The class members as [first, last] code points, in written order. */
function writtenRanges (source: string): [number, number][] {
const members: [number, number][] = [];
let rebuilt = '[';
for (const [ text, start, end ] of source.matchAll(/\\u([0-9a-f]{4})(?:-\\u([0-9a-f]{4}))?/g)) {
members.push([ parseInt(start, 16), parseInt(end ?? start, 16) ]);
rebuilt += text;
}
expect(rebuilt + ']').toBe(source);
return members;
}

describe('reRefusedAbove00AF', () => {
const matchedRuns: [number, number][] = [];
let matchedCount = 0;
for (let codePoint = 0; codePoint <= 0x10ffff; codePoint++) {
if (!reRefusedAbove00AF.test(String.fromCodePoint(codePoint))) {
continue;
}
matchedCount++;
const lastRun = matchedRuns.at(-1);
if (lastRun && lastRun[1] === codePoint - 1) {
lastRun[1] = codePoint;
}
else {
matchedRuns.push([ codePoint, codePoint ]);
}
}

test('matches the 4,295 code points that Excel refuses', () => {
expect(matchedCount).toBe(4295);
expect(matchedRuns[0]).toEqual([ 0xbb, 0xbb ]);
expect(matchedRuns.at(-1)).toEqual([ 0xffef, 0xfff8 ]);
});

test('matches no surrogate or noncharacter', () => {
const refuses = (from: number, to: number) => matchedRuns.some(([ start, end ]) => start <= to && end >= from);
expect(refuses(0xd800, 0xdfff), 'surrogates').toBe(false);
expect(refuses(0xfdd0, 0xfdef), 'noncharacters U+FDD0-U+FDEF').toBe(false);
expect(refuses(0xfffe, 0xffff), 'noncharacters U+FFFE and U+FFFF').toBe(false);
});

test('is written as 471 sorted, disjoint, non-adjacent ranges', () => {
const written = writtenRanges(reRefusedAbove00AF.source);
expect(written.length).toBe(471);
expect(written).toEqual(matchedRuns);
});
});
8 changes: 8 additions & 0 deletions lib/quoteRequiringCharacters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Code points above U+00AF that Excel refuses in an unquoted sheet or workbook prefix, found by
* testing every code point in U+00AF-U+FFFF. Nothing assigned above U+FFFF is refused.
*/
// The members are escapes, so no combining mark can combine with its neighbour.
/* eslint-disable no-misleading-character-class */
export const reRefusedAbove00AF =
/[\u00bb\u02ef-\u02ff\u034f-\u035f\u0363-\u036f\u0378-\u0379\u037e\u0380-\u0383\u0387\u038b\u038d\u03a2\u03f6\u0487\u0530\u0557-\u0558\u055a-\u055f\u0589-\u0590\u05a2\u05ba\u05be\u05c0\u05c3\u05c5-\u05cf\u05eb-\u05ee\u05f3-\u061e\u0656-\u065f\u066a-\u066d\u06d4\u0700-\u070e\u074b-\u074c\u07b2-\u07c9\u07eb-\u07f3\u07f6-\u07f9\u07fb-\u07ff\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u083f\u0859-\u085f\u086b-\u086f\u0888\u088f-\u089f\u08ca-\u0900\u093a-\u093b\u094e-\u094f\u0955-\u0957\u0964-\u0965\u0970\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09bb\u09c5-\u09c6\u09c9-\u09ca\u09cf-\u09d6\u09d8-\u09db\u09de\u09e4-\u09e5\u09fb\u09fd-\u0a01\u0a03-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a3b\u0a3d\u0a43-\u0a46\u0a49-\u0a4a\u0a4e-\u0a58\u0a5d\u0a5f-\u0a65\u0a75-\u0a80\u0a84\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abb\u0ac6\u0aca\u0ace-\u0acf\u0ad1-\u0adf\u0ae2-\u0ae5\u0af0-\u0af8\u0afa-\u0b00\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34\u0b3a-\u0b3b\u0b44-\u0b46\u0b49-\u0b4a\u0b4e-\u0b55\u0b58-\u0b5b\u0b5e\u0b62-\u0b65\u0b78-\u0b81\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bba-\u0bbd\u0bc3-\u0bc5\u0bc9\u0bce-\u0bcf\u0bd1-\u0bd6\u0bd8-\u0be5\u0bf3-\u0c00\u0c04\u0c0d\u0c11\u0c29\u0c3a-\u0c3c\u0c45\u0c49\u0c4e-\u0c54\u0c57\u0c5b-\u0c5c\u0c5e-\u0c5f\u0c62-\u0c65\u0c70-\u0c77\u0c7f\u0c81\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cbc\u0cc5\u0cc9\u0cce-\u0cd4\u0cd7-\u0cdc\u0cdf\u0ce2-\u0ce5\u0cf0\u0cf3-\u0d01\u0d0d\u0d11\u0d3b-\u0d3c\u0d44-\u0d45\u0d49\u0d4f-\u0d53\u0d62-\u0d65\u0d79\u0d80-\u0d81\u0d84\u0d97-\u0d99\u0db2\u0dbc\u0dbe-\u0dbf\u0dc7-\u0dc9\u0dcb-\u0dce\u0dd5\u0dd7\u0de0-\u0df1\u0df4-\u0e00\u0e3b-\u0e3e\u0e4f\u0e5a-\u0e80\u0e83\u0e85\u0e8b\u0ea4\u0ea6\u0eba\u0ebe-\u0ebf\u0ec5\u0ec7\u0ece-\u0ecf\u0eda-\u0edb\u0ee0-\u0eff\u0f04-\u0f12\u0f3a-\u0f3d\u0f48\u0f6d-\u0f70\u0f85\u0f8d-\u0f8f\u0f98\u0fbd\u0fcd-\u0fce\u0fd0-\u0fff\u102b\u1033-\u1035\u103a-\u103e\u104a-\u104f\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109f\u10c6\u10c8-\u10cc\u10ce-\u10cf\u10fb\u1249\u124e-\u124f\u1257\u1259\u125e-\u125f\u1289\u128e-\u128f\u12b1\u12b6-\u12b7\u12bf\u12c1\u12c6-\u12c7\u12d7\u1311\u1316-\u1317\u135b-\u1368\u137d-\u137f\u1390-\u139f\u13f6-\u13f7\u13fe-\u1400\u166d-\u166e\u169b-\u169f\u16eb-\u16ed\u16f9-\u16ff\u1712-\u171e\u1732-\u173f\u1752-\u175f\u176d\u1771-\u177f\u17d4-\u17d6\u17d8-\u17da\u17dd-\u17df\u17ea-\u17ef\u17fa-\u180a\u180f\u181a-\u181f\u1879-\u187f\u18ab-\u18af\u18f6-\u18ff\u191f-\u194f\u196e-\u196f\u1975-\u197f\u19ac-\u19af\u19ca-\u19d9\u19db-\u19ff\u1a17-\u1a1f\u1a55-\u1aa6\u1aa8-\u1b04\u1b34-\u1b44\u1b4d-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bff\u1c24-\u1c4c\u1c50-\u1c59\u1c7e-\u1c7f\u1c8b-\u1c8f\u1cbb-\u1cbc\u1cc0-\u1ce8\u1ced\u1cf4\u1cf7-\u1cf9\u1cfb-\u1cff\u1dc0-\u1dff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fc5\u1fd4-\u1fd5\u1fdc\u1ff0-\u1ff1\u1ff5\u1fff\u2011-\u2012\u2017\u201a-\u201b\u201e-\u201f\u2022-\u2024\u2031\u2034\u2036-\u203a\u203c-\u2043\u2045-\u2069\u2072-\u2073\u207d-\u207e\u208d-\u208f\u209d-\u209f\u20b0-\u20cf\u20e4-\u20ff\u213b\u2140-\u2144\u214a-\u214d\u214f\u218a-\u218f\u21f4-\u21ff\u22f2-\u22ff\u2329-\u232a\u237c\u239b-\u23ff\u2427-\u243f\u244b-\u245f\u2596-\u259f\u25f8-\u25ff\u2614-\u2618\u2672-\u2700\u2705\u270a-\u270b\u2728\u274c\u274e\u2753-\u2755\u2757\u275f-\u2760\u2768-\u2775\u2795-\u2797\u27b0\u27bf-\u27ff\u2900-\u2bff\u2ce5-\u2cea\u2cef-\u2cf1\u2cf4-\u2cfc\u2cfe-\u2cff\u2d26\u2d28-\u2d2c\u2d2e-\u2d2f\u2d68-\u2d6e\u2d70-\u2d7f\u2d97-\u2d9f\u2da7\u2daf\u2db7\u2dbf\u2dc7\u2dcf\u2dd7\u2ddf-\u2e2e\u2e30-\u2e7f\u2e9a\u2ef4-\u2eff\u2fe0-\u2fef\u3018-\u301c\u3030\u303d\u3040\u3097-\u3098\u30a0\u3100-\u3104\u3130\u318f\u31c0-\u31ef\u321d-\u321f\u3244-\u3247\u3250\u327c-\u327e\u32cc-\u32cf\u32ff\u3377-\u337a\u33de-\u33df\u33ff\u4dc0-\u4dff\ua48d-\ua48f\ua4a2-\ua4a3\ua4b4\ua4c1\ua4c5\ua4c7-\ua4cf\ua4fe-\ua4ff\ua60d-\ua60f\ua620-\ua629\ua62c-\ua63f\ua66f-\ua67e\ua69e-\ua69f\ua6f0-\ua716\ua720-\ua721\ua789-\ua78a\ua7ce-\ua7cf\ua7d2\ua7d4\ua7dd-\ua7f1\ua802\ua806\ua80b\ua823-\ua82f\ua836-\ua83f\ua874-\ua881\ua8b4-\ua8f1\ua8f8-\ua8fa\ua8fc\ua8ff-\ua909\ua926-\ua92f\ua947-\ua95f\ua97d-\ua983\ua9b3-\ua9ce\ua9d0-\ua9df\ua9e5\ua9f0-\ua9f9\ua9ff\uaa29-\uaa3f\uaa43\uaa4c-\uaa5f\uaa77-\uaa79\uaa7b-\uaa7d\uaab0\uaab2-\uaab4\uaab7-\uaab8\uaabe-\uaabf\uaac1\uaac3-\uaada\uaade-\uaadf\uaaeb-\uaaf1\uaaf5-\uab00\uab07-\uab08\uab0f-\uab10\uab17-\uab1f\uab27\uab2f\uab5b\uab6a-\uab6f\uabe3-\uabff\ud7a4-\ud7af\ud7c7-\ud7ca\ud7fc-\ud7ff\ufa6e-\ufa6f\ufada-\ufaff\ufb07-\ufb12\ufb18-\ufb1c\ufb37\ufb3d\ufb3f\ufb42\ufb45\ufbb2-\ufbd2\ufd3e-\ufd4f\ufd90-\ufd91\ufdc8-\ufdcf\ufdfc-\ufe1f\ufe24-\ufe2f\ufe32\ufe45-\ufe48\ufe53\ufe58\ufe67\ufe6c-\ufe6f\ufe75\ufefd-\ufefe\uff00\uff5f-\uff60\uffbf-\uffc1\uffc8-\uffc9\uffd0-\uffd1\uffd8-\uffd9\uffdd-\uffdf\uffe7\uffef-\ufff8]/;
20 changes: 20 additions & 0 deletions lib/stringifyA1Ref.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ describe('stringifyA1Ref', () => {
expect(stringifyA1Ref({ context: [ 'Truer' ], range: rangeA1 })).toBe('Truer!A1');
});

test('quotes characters above U+00AF that Excel refuses unquoted', () => {
expect(stringifyA1Ref({ context: [ 'My\u00bbSheet' ], range: rangeA1 })).toBe("'My\u00bbSheet'!A1");
expect(stringifyA1Ref({ context: [ 'My\u02efSheet' ], range: rangeA1 })).toBe("'My\u02efSheet'!A1");
expect(stringifyA1Ref({ context: [ 'My\ufff8Sheet' ], range: rangeA1 })).toBe("'My\ufff8Sheet'!A1");
expect(stringifyA1Ref({ context: [ 'Jan:My\u00bbSheet' ], range: rangeA1 })).toBe("'Jan:My\u00bbSheet'!A1");
expect(stringifyA1Ref({ context: [ 'My\u00baSheet' ], range: rangeA1 })).toBe('My\u00baSheet!A1');
expect(stringifyA1Ref({ context: [ 'My\u00bcSheet' ], range: rangeA1 })).toBe('My\u00bcSheet!A1');
expect(stringifyA1Ref({ context: [ 'My\u02eeSheet' ], range: rangeA1 })).toBe('My\u02eeSheet!A1');
expect(stringifyA1Ref({ context: [ 'My\ufff9Sheet' ], range: rangeA1 })).toBe('My\ufff9Sheet!A1');
expect(stringifyA1Ref({ context: [ 'My\u{1f600}Sheet' ], range: rangeA1 })).toBe('My\u{1f600}Sheet!A1');
});

test('3D references are quoted per endpoint', () => {
expect(stringifyA1Ref({ context: [ 'Jan:Dec' ], range: rangeA1 })).toBe('Jan:Dec!A1');
expect(stringifyA1Ref({ context: [ 'Sheet 1:Sheet 2' ], range: rangeA1 })).toBe("'Sheet 1:Sheet 2'!A1");
Expand Down Expand Up @@ -148,6 +160,14 @@ describe('stringifyA1Ref in XLSX mode', () => {
expect(stringifyA1RefXlsx({ sheetName: '1040', name: 'foo' })).toBe("'1040'!foo");
});

test('quotes characters above U+00AF that Excel refuses unquoted', () => {
expect(stringifyA1RefXlsx({ sheetName: 'My\u00bbSheet', range: rangeA1 })).toBe("'My\u00bbSheet'!A1");
expect(stringifyA1RefXlsx({ sheetName: 'My\u00bcSheet', range: rangeA1 })).toBe('My\u00bcSheet!A1');
expect(stringifyA1RefXlsx({ workbookName: 'My\u00bbBook.xlsx', sheetName: 'Sheet1', range: rangeA1 })).toBe("'[My\u00bbBook.xlsx]Sheet1'!A1");
expect(stringifyA1RefXlsx({ workbookName: 'My\u00bcBook.xlsx', sheetName: 'Sheet1', range: rangeA1 })).toBe('[My\u00bcBook.xlsx]Sheet1!A1');
expect(stringifyA1RefXlsx({ sheetName: 'My\u00bbSheet', name: 'foo' })).toBe("'My\u00bbSheet'!foo");
});

test('an external link index is not quoted', () => {
expect(stringifyA1RefXlsx({ workbookName: '1', sheetName: 'Sheet1', range: rangeA1 })).toBe('[1]Sheet1!A1');
expect(stringifyA1RefXlsx({ workbookName: '12', sheetName: 'Sheet1', range: rangeA1 })).toBe('[12]Sheet1!A1');
Expand Down
3 changes: 2 additions & 1 deletion lib/stringifyPrefix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
ReferenceName,
ReferenceNameXlsx
} from './types.ts';
import { reRefusedAbove00AF } from './quoteRequiringCharacters.ts';

const CHAR_PERIOD = 46;
const CHAR_0 = 48;
Expand All @@ -34,7 +35,7 @@ export function needQuotes (scope: string, blockSheetRanges: boolean, bracketed:
}
return bits.some(bit => needQuotes(bit, blockSheetRanges, bracketed)) ? 1 : 0;
}
if (reBannedChars.test(scope)) {
if (reBannedChars.test(scope) || reRefusedAbove00AF.test(scope)) {
return 1;
}
if (reIsRangelike.test(scope)) {
Expand Down
7 changes: 7 additions & 0 deletions lib/translateToA1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,13 @@ describe('translate 3D references', () => {
isR2A('=A:C!R1C1', 'A1', "='A:C'!$A$1");
isR2A("=SUM('Sheet 1:Sheet 2'!R1C1:R2C2)", 'A1', "=SUM('Sheet 1:Sheet 2'!$A$1:$B$2)");
});

test('keeps the quotes on a name with a character that Excel refuses unquoted', () => {
isR2A("='My\u00bbSheet'!RC[-2]", 'C1', "='My\u00bbSheet'!A1");
isR2A("='My\u00bcSheet'!RC[-2]", 'C1', '=My\u00bcSheet!A1');
isR2A("=SUM('Alpha:My\u00bbSheet'!R1C1)", 'A1', "=SUM('Alpha:My\u00bbSheet'!$A$1)");
isR2A("='[My\u00bbBook.xlsx]Sheet1'!R1C1", 'A1', "='[My\u00bbBook.xlsx]Sheet1'!$A$1");
});
});

describe('translate r & c as LET parameters', () => {
Expand Down