Skip to content

Commit

Permalink
chore: Move natural-sort functionality into packages/internal (#10320)
Browse files Browse the repository at this point in the history
Ref endojs/endo#2113

## Description
Removes unnecessary use of `localeCompare` and consolidates natural-order sorting.

### Security Considerations
n/a

### Scaling Considerations
n/a

### Documentation Considerations
n/a

### Testing Considerations
None in particular, although I probably should add something to packages/internal/test.

### Upgrade Considerations
This code runs outside of all vats (and AFAIK is not exercised on-chain at all) and is safe to include in any release.
  • Loading branch information
mergify[bot] authored Feb 3, 2025
2 parents 048a67c + b567001 commit 8093cf6
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 77 deletions.
38 changes: 6 additions & 32 deletions packages/SwingSet/src/kernel/state/kernelKeeper.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Nat, isNat } from '@endo/nat';
import { assert, Fail } from '@endo/errors';
import { naturalCompare } from '@agoric/internal/src/natural-sort.js';
import {
initializeVatState,
makeVatKeeper,
Expand Down Expand Up @@ -1886,39 +1887,12 @@ export default function makeKernelKeeper(
}
}

function compareNumbers(a, b) {
return Number(a - b);
}

function compareStrings(a, b) {
// natural-sort strings having a shared prefix followed by digits
// (e.g., 'ko42' and 'ko100')
const [_a, aPrefix, aDigits] = /^(\D+)(\d+)$/.exec(a) || [];
if (aPrefix) {
const [_b, bPrefix, bDigits] = /^(\D+)(\d+)$/.exec(b) || [];
if (bPrefix === aPrefix) {
return compareNumbers(aDigits, bDigits);
}
}

// otherwise use the default string ordering
if (a > b) {
return 1;
}
if (a < b) {
return -1;
}
return 0;
}

// Perform an element-by-element natural sort.
kernelTable.sort(
(a, b) =>
compareStrings(a[0], b[0]) ||
compareStrings(a[1], b[1]) ||
compareNumbers(a[2], b[2]) ||
compareStrings(a[3], b[3]) ||
compareNumbers(a[4], b[4]) ||
compareNumbers(a[5], b[5]) ||
naturalCompare(a[0], b[0]) ||
naturalCompare(a[1], b[1]) ||
naturalCompare(a[2], b[2]) ||
0,
);

Expand All @@ -1931,7 +1905,7 @@ export default function makeKernelKeeper(
promises.push({ id: kpid, ...getKernelPromise(kpid) });
}
}
promises.sort((a, b) => compareStrings(a.id, b.id));
promises.sort((a, b) => naturalCompare(a.id, b.id));

const objects = [];
const nextObjectID = Nat(BigInt(getRequired('ko.nextID')));
Expand Down
48 changes: 48 additions & 0 deletions packages/internal/src/natural-sort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @param {string} a
* @param {string} b
* @returns {-1 | 0 | 1}
*/
const compareNats = (a, b) => {
// Default to IEEE 754 number arithmetic for speed, but fall back on bigint
// arithmetic to resolve ties because big numbers can lose resolution
// (sometimes even becoming infinite) and then ultimately on length to resolve
// ties by ascending count of leading zeros.
const diff = +a - +b;
const finiteDiff =
(Number.isFinite(diff) && diff) ||
(a === b ? 0 : Number(BigInt(a) - BigInt(b)) || a.length - b.length);

// @ts-expect-error this call really does return -1 | 0 | 1
return Math.sign(finiteDiff);
};

// TODO: compareByCodePoints
// https://github.com/endojs/endo/pull/2008
// eslint-disable-next-line no-nested-ternary
const compareStrings = (a, b) => (a > b ? 1 : a < b ? -1 : 0);

const rPrefixedDigits = /^(\D*)(\d+)(\D.*|)/s;

/**
* Perform a single-level natural-sort comparison, finding the first decimal
* digit sequence in each operand and comparing first by the (possibly empty)
* preceding prefix as strings, then by the digits as integers, then by any
* following suffix (e.g., sorting 'ko42' before 'ko100' as ['ko', 42] vs.
* ['ko', 100]).
*
* @param {string} a
* @param {string} b
* @returns {-1 | 0 | 1}
*/
export const naturalCompare = (a, b) => {
const [_a, aPrefix, aDigits, aSuffix] = rPrefixedDigits.exec(a) || [];
if (aPrefix !== undefined) {
const [_b, bPrefix, bDigits, bSuffix] = rPrefixedDigits.exec(b) || [];
if (bPrefix === aPrefix) {
return compareNats(aDigits, bDigits) || compareStrings(aSuffix, bSuffix);
}
}
return compareStrings(a, b);
};
harden(naturalCompare);
56 changes: 11 additions & 45 deletions packages/swingset-runner/src/dumpstore.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'fs';
import process from 'process';
import { naturalCompare } from '@agoric/internal/src/natural-sort.js';

export function dumpStore(kernelStorage, outfile, rawMode, truncate = true) {
const transcriptStore = kernelStorage.transcriptStore;
Expand Down Expand Up @@ -214,55 +215,20 @@ export function dumpStore(kernelStorage, outfile, rawMode, truncate = true) {
}

function* groupKeys(baseKey) {
const subkeys = Array.from(state.keys()).sort();
const end = `${baseKey}~`;
for (const key of subkeys) {
if (baseKey <= key && key < end) {
yield key;
}
// TODO: compareByCodePoints
// https://github.com/endojs/endo/pull/2008
const sortedKeys = [...state.keys()].sort();
for (const key of sortedKeys) {
if (key < baseKey) continue;
if (!key.startsWith(baseKey)) break;
yield key;
}
}

function pgroup(baseKey) {
const toSort = [];
for (const key of groupKeys(baseKey)) {
toSort.push([key, eat(key)]);
}
// sort similar keys by their numeric portions if possible, e.g.,
// "ko7.owner" should be less than "ko43.owner" even though it would be the
// other way around if they were simply compared as strings.
toSort.sort((elem1, elem2) => {
// chop off baseKey, since it's not useful for comparisons
const key1 = elem1[0].slice(baseKey.length);
const key2 = elem2[0].slice(baseKey.length);

// find the first non leading digit position in each key
const cut1 = key1.search(/[^0-9]/);
const cut2 = key2.search(/[^0-9]/);

// if either key lacks leading digits, at least one of them has no number
// to compare, so just compare the keys themselves
if (cut1 === 0 || cut2 === 0) {
return key1.localeCompare(key2);
}

// treat the number parts as numbers
const num1 = Number(key1.substr(0, cut1));
const num2 = Number(key2.substr(0, cut2));

if (num1 !== num2) {
// if the numbers are different, the comparison is the comparison of the
// numbers
return num1 - num2;
} else {
// if the numbers are the same, the comparison is the comparison of the
// remainder of the key, which, because of how we got here, is the same
// as comparing the whole key
return key1.localeCompare(key2);
}
});
for (const [key, value] of toSort) {
pkv(key, value);
const sortedKeys = [...groupKeys(baseKey)].sort(naturalCompare);
for (const key of sortedKeys) {
pkv(key, eat(key));
}
}

Expand Down

0 comments on commit 8093cf6

Please sign in to comment.