Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
33 changes: 26 additions & 7 deletions dev/src/order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,19 +248,38 @@ function compareVectors(left: ApiMapValue, right: ApiMapValue): number {
return compareArrays(leftArray, rightArray);
}

function stringToUtf8Bytes(str: string): Uint8Array {
return new TextEncoder().encode(str);
}

/*!
* Compare strings in UTF-8 encoded byte order
* @private
* @internal
*/
export function compareUtf8Strings(left: string, right: string): number {
const leftBytes = stringToUtf8Bytes(left);
const rightBytes = stringToUtf8Bytes(right);
return compareBlobs(Buffer.from(leftBytes), Buffer.from(rightBytes));
let i = 0;
while (i < left.length && i < right.length) {
const leftCodePoint = left.codePointAt(i)!;
const rightCodePoint = right.codePointAt(i)!;

if (leftCodePoint !== rightCodePoint) {
if (leftCodePoint < 128 && rightCodePoint < 128) {
// ASCII comparison
return primitiveComparator(leftCodePoint, rightCodePoint);
} else {
// Lazy instantiate TextEncoder
const encoder = new TextEncoder();

// UTF-8 encoded byte comparison, substring 2 indexes to cover surrogate pairs
const leftBytes = encoder.encode(left.substring(i, i + 2));
const rightBytes = encoder.encode(right.substring(i, i + 2));
return compareBlobs(Buffer.from(leftBytes), Buffer.from(rightBytes));
}
}

// Increment by 2 for surrogate pairs, 1 otherwise
i += leftCodePoint > 0xffff ? 2 : 1;
}

// Compare lengths if all characters are equal
return primitiveComparator(left.length, right.length);
}

/*!
Expand Down
42 changes: 38 additions & 4 deletions dev/system-test/firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4086,6 +4086,20 @@ describe('Query class', () => {
});

describe('sort unicode strings', () => {
const expectedDocs = [
'b',
'a',
'h',
'i',
'c',
'f',
'e',
'd',
'g',
'k',
'j',
];

it('snapshot listener sorts unicode strings same as server', async () => {
const collection = await testCollectionWithDocs({
a: {value: 'Łukasiewicz'},
Expand All @@ -4095,10 +4109,13 @@ describe('Query class', () => {
e: {value: 'P'},
f: {value: '︒'},
g: {value: '🐵'},
h: {value: '你好'},
i: {value: '你顥'},
j: {value: '😁'},
k: {value: '😀'},
});

const query = collection.orderBy('value');
const expectedDocs = ['b', 'a', 'c', 'f', 'e', 'd', 'g'];

const getSnapshot = await query.get();
expect(getSnapshot.docs.map(d => d.id)).to.deep.equal(expectedDocs);
Expand All @@ -4123,10 +4140,13 @@ describe('Query class', () => {
e: {value: ['P']},
f: {value: ['︒']},
g: {value: ['🐵']},
h: {value: ['你好']},
i: {value: ['你顥']},
j: {value: ['😁']},
k: {value: ['😀']},
});

const query = collection.orderBy('value');
const expectedDocs = ['b', 'a', 'c', 'f', 'e', 'd', 'g'];

const getSnapshot = await query.get();
expect(getSnapshot.docs.map(d => d.id)).to.deep.equal(expectedDocs);
Expand All @@ -4151,10 +4171,13 @@ describe('Query class', () => {
e: {value: {foo: 'P'}},
f: {value: {foo: '︒'}},
g: {value: {foo: '🐵'}},
h: {value: {foo: '你好'}},
i: {value: {foo: '你顥'}},
j: {value: {foo: '😁'}},
k: {value: {foo: '😀'}},
});

const query = collection.orderBy('value');
const expectedDocs = ['b', 'a', 'c', 'f', 'e', 'd', 'g'];

const getSnapshot = await query.get();
expect(getSnapshot.docs.map(d => d.id)).to.deep.equal(expectedDocs);
Expand All @@ -4179,10 +4202,13 @@ describe('Query class', () => {
e: {value: {P: true}},
f: {value: {'︒': true}},
g: {value: {'🐵': true}},
h: {value: {你好: true}},
i: {value: {你顥: true}},
j: {value: {'😁': true}},
k: {value: {'😀': true}},
});

const query = collection.orderBy('value');
const expectedDocs = ['b', 'a', 'c', 'f', 'e', 'd', 'g'];

const getSnapshot = await query.get();
expect(getSnapshot.docs.map(d => d.id)).to.deep.equal(expectedDocs);
Expand All @@ -4207,17 +4233,25 @@ describe('Query class', () => {
P: {value: true},
'︒': {value: true},
'🐵': {value: true},
你好: {value: true},
你顥: {value: true},
'😁': {value: true},
'😀': {value: true},
});

const query = collection.orderBy(FieldPath.documentId());
const expectedDocs = [
'Sierpiński',
'Łukasiewicz',
'你好',
'你顥',
'岩澤',
'︒',
'P',
'🄟',
'🐵',
'😀',
'😁',
];

const getSnapshot = await query.get();
Expand Down