Skip to content

Commit

Permalink
Merge pull request #92 from mcpower/from-string-cache
Browse files Browse the repository at this point in the history
Add cache for fromString
  • Loading branch information
Patashu authored Jun 28, 2022
2 parents 0684eec + d99422c commit 2d464f6
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 3 deletions.
70 changes: 67 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { LRUCache } from "./lru-cache";

export type CompareResult = -1 | 0 | 1;

const MAX_SIGNIFICANT_DIGITS = 17; //Maximum number of digits of precision to assume in Number
Expand All @@ -14,6 +16,8 @@ const NUMBER_EXP_MIN = -324; //The smallest exponent that can appear in a Number

const MAX_ES_IN_A_ROW = 5; //For default toString behaviour, when to swap from eee... to (e^n) syntax.

const DEFAULT_FROM_STRING_CACHE_SIZE = (1 << 10) - 1; // The default size of the LRU cache used to cache Decimal.fromString.

const IGNORE_COMMAS = true;
const COMMAS_ARE_DECIMAL_POINTS = false;

Expand Down Expand Up @@ -348,6 +352,8 @@ export default class Decimal {
public static readonly dNumberMax = FC(1, 0, Number.MAX_VALUE);
public static readonly dNumberMin = FC(1, 0, Number.MIN_VALUE);

private static fromStringCache = new LRUCache<string, Decimal>(DEFAULT_FROM_STRING_CACHE_SIZE);

public sign = 0;
public mag = 0;
public layer = 0;
Expand Down Expand Up @@ -485,7 +491,22 @@ export default class Decimal {
* is required.
*/
public static fromValue_noAlloc(value: DecimalSource): Readonly<Decimal> {
return value instanceof Decimal ? value : new Decimal(value);
if (value instanceof Decimal) {
return value;
} else if (typeof value === "string") {
const cached = Decimal.fromStringCache.get(value);
if (cached !== undefined) {
return cached;
}
return Decimal.fromString(value);
} else if (typeof value === "number") {
return Decimal.fromNumber(value);
} else {
// This should never happen... but some users like Prestige Tree Rewritten
// pass undefined values in as DecimalSources, so we should handle this
// case to not break them.
return Decimal.dZero;
}
}

public static abs(value: DecimalSource): Decimal {
Expand Down Expand Up @@ -1152,6 +1173,11 @@ export default class Decimal {
}

public fromString(value: string): Decimal {
const originalValue = value;
const cached = Decimal.fromStringCache.get(originalValue);
if (cached !== undefined) {
return this.fromDecimal(cached);
}
if (IGNORE_COMMAS) {
value = value.replace(",", "");
} else if (COMMAS_ARE_DECIMAL_POINTS) {
Expand All @@ -1176,6 +1202,9 @@ export default class Decimal {
this.sign = result.sign;
this.layer = result.layer;
this.mag = result.mag;
if (Decimal.fromStringCache.maxSize >= 1) {
Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this));
}
return this;
}
}
Expand All @@ -1198,6 +1227,9 @@ export default class Decimal {
this.sign = result.sign;
this.layer = result.layer;
this.mag = result.mag;
if (Decimal.fromStringCache.maxSize >= 1) {
Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this));
}
return this;
}
}
Expand All @@ -1212,6 +1244,9 @@ export default class Decimal {
this.sign = result.sign;
this.layer = result.layer;
this.mag = result.mag;
if (Decimal.fromStringCache.maxSize >= 1) {
Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this));
}
return this;
}
}
Expand All @@ -1237,6 +1272,9 @@ export default class Decimal {
this.sign = result.sign;
this.layer = result.layer;
this.mag = result.mag;
if (Decimal.fromStringCache.maxSize >= 1) {
Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this));
}
return this;
}
}
Expand All @@ -1257,6 +1295,9 @@ export default class Decimal {
this.sign = result.sign;
this.layer = result.layer;
this.mag = result.mag;
if (Decimal.fromStringCache.maxSize >= 1) {
Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this));
}
return this;
}
}
Expand All @@ -1268,13 +1309,21 @@ export default class Decimal {
if (ecount === 0) {
const numberAttempt = parseFloat(value);
if (isFinite(numberAttempt)) {
return this.fromNumber(numberAttempt);
this.fromNumber(numberAttempt);
if (Decimal.fromStringCache.size >= 1) {
Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this));
}
return this;
}
} else if (ecount === 1) {
//Very small numbers ("2e-3000" and so on) may look like valid floats but round to 0.
const numberAttempt = parseFloat(value);
if (isFinite(numberAttempt) && numberAttempt !== 0) {
return this.fromNumber(numberAttempt);
this.fromNumber(numberAttempt);
if (Decimal.fromStringCache.maxSize >= 1) {
Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this));
}
return this;
}
}

Expand All @@ -1296,6 +1345,9 @@ export default class Decimal {
this.layer = parseFloat(layerstring);
this.mag = parseFloat(newparts[1].substr(i + 1));
this.normalize();
if (Decimal.fromStringCache.maxSize >= 1) {
Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this));
}
return this;
}
}
Expand All @@ -1305,13 +1357,19 @@ export default class Decimal {
this.sign = 0;
this.layer = 0;
this.mag = 0;
if (Decimal.fromStringCache.maxSize >= 1) {
Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this));
}
return this;
}
const mantissa = parseFloat(parts[0]);
if (mantissa === 0) {
this.sign = 0;
this.layer = 0;
this.mag = 0;
if (Decimal.fromStringCache.maxSize >= 1) {
Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this));
}
return this;
}
let exponent = parseFloat(parts[parts.length - 1]);
Expand Down Expand Up @@ -1346,6 +1404,9 @@ export default class Decimal {
this.sign = result.sign;
this.layer = result.layer;
this.mag = result.mag;
if (Decimal.fromStringCache.maxSize >= 1) {
Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this));
}
return this;
} else {
//at eee and above, mantissa is too small to be recognizable!
Expand All @@ -1354,6 +1415,9 @@ export default class Decimal {
}

this.normalize();
if (Decimal.fromStringCache.maxSize >= 1) {
Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this));
}
return this;
}

Expand Down
139 changes: 139 additions & 0 deletions src/lru-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* A LRU cache intended for caching pure functions.
*/
export class LRUCache<K, V> {
private map = new Map<K, ListNode<K, V>>();
// Invariant: Exactly one of the below is true before and after calling a
// LRUCache method:
// - first and last are both undefined, and map.size() is 0.
// - first and last are the same object, and map.size() is 1.
// - first and last are different objects, and map.size() is greater than 1.
private first: ListNode<K, V> | undefined = undefined;
private last: ListNode<K, V> | undefined = undefined;
maxSize: number;

/**
* @param maxSize The maximum size for this cache. We recommend setting this
* to be one less than a power of 2, as most hashtables - including V8's
* Object hashtable (https://crsrc.org/c/v8/src/objects/ordered-hash-table.cc)
* - uses powers of two for hashtable sizes. It can't exactly be a power of
* two, as a .set() call could temporarily set the size of the map to be
* maxSize + 1.
*/
constructor(maxSize: number) {
this.maxSize = maxSize;
}

get size(): number {
return this.map.size;
}

/**
* Gets the specified key from the cache, or undefined if it is not in the
* cache.
* @param key The key to get.
* @returns The cached value, or undefined if key is not in the cache.
*/
get(key: K): V | undefined {
const node = this.map.get(key);
if (node === undefined) {
return undefined;
}
// It is guaranteed that there is at least one item in the cache.
// Therefore, first and last are guaranteed to be a ListNode...
// but if there is only one item, they might be the same.

// Update the order of the list to make this node the first node in the
// list.
// This isn't needed if this node is already the first node in the list.
if (node !== this.first) {
// As this node is DIFFERENT from the first node, it is guaranteed that
// there are at least two items in the cache.
// However, this node could possibly be the last item.
if (node === this.last) {
// This node IS the last node.
this.last = node.prev;
// From the invariants, there must be at least two items in the cache,
// so node - which is the original "last node" - must have a defined
// previous node. Therefore, this.last - set above - must be defined
// here.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.last!.next = undefined;
} else {
// This node is somewhere in the middle of the list, so there must be at
// least THREE items in the list, and this node's prev and next must be
// defined here.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
node.prev!.next = node.next;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
node.next!.prev = node.prev;
}
node.next = this.first;
// From the invariants, there must be at least two items in the cache, so
// this.first must be a valid ListNode.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.first!.prev = node;
this.first = node;
}
return node.value;
}

/**
* Sets an entry in the cache.
*
* @param key The key of the entry.
* @param value The value of the entry.
* @throws Error, if the map already contains the key.
*/
set(key: K, value: V): void {
// Ensure that this.maxSize >= 1.
if (this.maxSize < 1) {
return;
}
if (this.map.has(key)) {
throw new Error("Cannot update existing keys in the cache");
}
const node = new ListNode(key, value);
// Move node to the front of the list.
if (this.first === undefined) {
// If the first is undefined, the last is undefined too.
// Therefore, this cache has no items in it.
this.first = node;
this.last = node;
} else {
// This cache has at least one item in it.
node.next = this.first;
this.first.prev = node;
this.first = node;
}
this.map.set(key, node);

while (this.map.size > this.maxSize) {
// We are guaranteed that this.maxSize >= 1,
// so this.map.size is guaranteed to be >= 2,
// so this.first and this.last must be different valid ListNodes,
// and this.last.prev must also be a valid ListNode (possibly this.first).
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const last = this.last!;
this.map.delete(last.key);
this.last = last.prev;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.last!.next = undefined;
}
}
}

/**
* A node in a doubly linked list.
*/
class ListNode<K, V> {
key: K;
value: V;
next: ListNode<K, V> | undefined = undefined;
prev: ListNode<K, V> | undefined = undefined;

constructor(key: K, value: V) {
this.key = key;
this.value = value;
}
}

0 comments on commit 2d464f6

Please sign in to comment.