Better collections; package types
This commit is contained in:
parent
55db55b42b
commit
b0ed7e914b
|
@ -9,6 +9,7 @@
|
|||
},
|
||||
"repository": "gitlab:preserves/preserves",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"author": "Tony Garnock-Jones <tonyg@leastfixedpoint.com>",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.19",
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
underlying,
|
||||
Annotated,
|
||||
Dictionary, Set, Bytes, Record, Single, Double,
|
||||
isSet, isDictionary,
|
||||
BytesLike,
|
||||
Value,
|
||||
} from './values';
|
||||
|
@ -117,8 +116,8 @@ export class Decoder {
|
|||
return this.includeAnnotations ? new Annotated(v) : v;
|
||||
}
|
||||
|
||||
static dictionaryFromArray(vs: Value[]): Dictionary {
|
||||
const d = new Dictionary();
|
||||
static dictionaryFromArray(vs: Value[]): Dictionary<Value> {
|
||||
const d = new Dictionary<Value>();
|
||||
if (vs.length % 2) throw new DecodeError("Missing dictionary value");
|
||||
for (let i = 0; i < vs.length; i += 2) {
|
||||
d.set(vs[i], vs[i+1]);
|
||||
|
@ -294,7 +293,7 @@ export class Encoder {
|
|||
this.emitbyte(Tag.End);
|
||||
}
|
||||
|
||||
push(v: Value) {
|
||||
push(v: any) {
|
||||
if (typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function') {
|
||||
v[PreserveOn](this);
|
||||
}
|
||||
|
@ -327,30 +326,6 @@ export class Encoder {
|
|||
else if (Array.isArray(v)) {
|
||||
this.encodevalues(Tag.Sequence, v);
|
||||
}
|
||||
else if (isSet(v)) {
|
||||
if (this.canonical) {
|
||||
const pieces = v._map((_v, k) => encode(k, { canonical: true }));
|
||||
pieces.sort(Bytes.compare);
|
||||
this.encoderawvalues(Tag.Set, pieces);
|
||||
} else {
|
||||
this.encodevalues(Tag.Set, v);
|
||||
}
|
||||
}
|
||||
else if (isDictionary(v)) {
|
||||
if (this.canonical) {
|
||||
const pieces = v._map((v, k) => Bytes.concat([encode(k, { canonical: true }),
|
||||
encode(v, { canonical: true })]));
|
||||
pieces.sort(Bytes.compare);
|
||||
this.encoderawvalues(Tag.Dictionary, pieces);
|
||||
} else {
|
||||
this.emitbyte(Tag.Dictionary);
|
||||
v._forEach((v, k) => {
|
||||
this.push(k);
|
||||
this.push(v);
|
||||
});
|
||||
this.emitbyte(Tag.End);
|
||||
}
|
||||
}
|
||||
else if (typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function') {
|
||||
this.encodevalues(Tag.Sequence, v as Iterable<Value>);
|
||||
}
|
||||
|
@ -361,10 +336,10 @@ export class Encoder {
|
|||
}
|
||||
}
|
||||
|
||||
export function encode(v: Value, options?: EncoderOptions): Bytes {
|
||||
export function encode(v: any, options?: EncoderOptions): Bytes {
|
||||
return new Encoder(options).push(v).contents();
|
||||
}
|
||||
|
||||
export function encodeWithAnnotations(v: Value, options: EncoderOptions = {}): Bytes {
|
||||
export function encodeWithAnnotations(v: any, options: EncoderOptions = {}): Bytes {
|
||||
return encode(v, { ... options, includeAnnotations: true });
|
||||
}
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
// FlexMap, FlexSet: like built-in Map and Set, but with a
|
||||
// canonicalization function which gives us the possibility of a
|
||||
// coarser equivalence than the identity equivalence used in Map and
|
||||
// Set.
|
||||
|
||||
// A Canonicalizer represents the equivalence you have in mind. For
|
||||
//
|
||||
// c: Canonicalizer<V>
|
||||
// eqv: Equivalence<V>
|
||||
// v1: V
|
||||
// v2: V
|
||||
//
|
||||
// where `eqv` is the equivalence you want,
|
||||
//
|
||||
// eqv(v1, v2) ⇔ c(v1) === c(v2)
|
||||
//
|
||||
export type Canonicalizer<V> = (v: V) => string;
|
||||
export type Equivalence<V> = (v1: V, v2: V) => boolean;
|
||||
|
||||
export type IdentityMap<K, V> = Map<K, V>;
|
||||
export type IdentitySet<V> = Set<V>;
|
||||
export const IdentityMap = Map;
|
||||
export const IdentitySet = Set;
|
||||
|
||||
export function _iterMap<S,T>(i: Iterator<S> | undefined, f : (s: S) => T): IterableIterator<T> {
|
||||
if (!i) return void 0;
|
||||
const _f = (r: IteratorResult<S>): IteratorResult<T> => {
|
||||
const { done, value } = r;
|
||||
return { done, value: done ? void 0 : f(value) };
|
||||
};
|
||||
return {
|
||||
next: (v?: any): IteratorResult<T> => _f(i.next(v)),
|
||||
return: (v?: any): IteratorResult<T> => _f(i.return(v)),
|
||||
throw: (e?: any): IteratorResult<T> => _f(i.throw(e)),
|
||||
[Symbol.iterator]() { return this; },
|
||||
};
|
||||
}
|
||||
|
||||
export class FlexMap<K, V> implements Map<K, V> {
|
||||
readonly items: Map<string, [K, V]>;
|
||||
readonly canonicalizer: Canonicalizer<K>;
|
||||
|
||||
constructor(c: Canonicalizer<K>, items?: Iterable<readonly [K, V]>) {
|
||||
this.canonicalizer = c;
|
||||
this.items = new Map((items === void 0)
|
||||
? void 0
|
||||
: _iterMap(items[Symbol.iterator](), ([k, v]) => [this._key(k), [k, v]]));
|
||||
}
|
||||
|
||||
_key(k: K): string {
|
||||
return this.canonicalizer(k);
|
||||
}
|
||||
|
||||
get(k: K, defaultValue?: V): V | undefined {
|
||||
const e = this.items.get(this._key(k));
|
||||
return (e === void 0) ? defaultValue : e[1];
|
||||
}
|
||||
|
||||
set(k: K, v: V): this {
|
||||
this.items.set(this._key(k), [k, v]);
|
||||
return this;
|
||||
}
|
||||
|
||||
forEach(f: <T extends Map<K, V>> (v: V, k: K, map: T) => void, thisArg?: any) {
|
||||
this.items.forEach(([k, v]) => f.call(thisArg, v, k, this));
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[K, V]> {
|
||||
return this.items.values();
|
||||
}
|
||||
|
||||
keys(): IterableIterator<K> {
|
||||
return _iterMap(this.items.values(), ([k, _v]) => k);
|
||||
}
|
||||
|
||||
values(): IterableIterator<V> {
|
||||
return _iterMap(this.items.values(), ([_k, v]) => v);
|
||||
}
|
||||
|
||||
delete(k: K): boolean {
|
||||
return this.items.delete(this._key(k));
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.items.clear();
|
||||
}
|
||||
|
||||
has(k: K): boolean {
|
||||
return this.items.has(this._key(k));
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.items.size;
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<[K, V]> {
|
||||
return this.items.values();
|
||||
}
|
||||
|
||||
[Symbol.toStringTag] = 'FlexMap';
|
||||
|
||||
equals(other: any, eqv: Equivalence<V> = (v1, v2) => v1 === v2): boolean {
|
||||
if (!('size' in other && 'has' in other && 'get' in other)) return false;
|
||||
if (this.size !== other.size) return false;
|
||||
for (let [k, v] of this.items.values()) {
|
||||
if (!other.has(k)) return false;
|
||||
if (!eqv(v, other.get(k))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
update(key: K,
|
||||
f: (oldValue?: V) => V | undefined,
|
||||
defaultValue?: V,
|
||||
eqv: Equivalence<V> = (v1, v2) => v1 === v2): number
|
||||
{
|
||||
const ks = this._key(key);
|
||||
if (this.items.has(ks)) {
|
||||
const oldValue = this.items.get(ks)[1];
|
||||
const newValue = f(oldValue);
|
||||
if (newValue === void 0) {
|
||||
this.items.delete(ks);
|
||||
return -1;
|
||||
} else {
|
||||
if (!eqv(newValue, oldValue)) this.items.set(ks, [key, newValue]);
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
const newValue = f(defaultValue);
|
||||
if (newValue === void 0) {
|
||||
return 0;
|
||||
} else {
|
||||
this.items.set(ks, [key, newValue]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canonicalKeys(): IterableIterator<string> {
|
||||
return this.items.keys();
|
||||
}
|
||||
}
|
||||
|
||||
export class FlexSet<V> implements Set<V> {
|
||||
readonly items: Map<string, V>;
|
||||
readonly canonicalizer: Canonicalizer<V>;
|
||||
|
||||
constructor(c: Canonicalizer<V>, items?: Iterable<V>) {
|
||||
this.canonicalizer = c;
|
||||
this.items = new Map((items === void 0)
|
||||
? void 0
|
||||
: _iterMap(items[Symbol.iterator](), (v) => [this._key(v), v]));
|
||||
}
|
||||
|
||||
_key(v: V): string {
|
||||
return this.canonicalizer(v);
|
||||
}
|
||||
|
||||
has(v: V): boolean {
|
||||
return this.items.has(this._key(v));
|
||||
}
|
||||
|
||||
get(v: V): {item: V} | null {
|
||||
const vs = this._key(v);
|
||||
if (this.items.has(vs)) {
|
||||
return {item: this.items[vs]};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
add(v: V): this {
|
||||
this.items[this._key(v)] = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
forEach(f: <T extends Set<V>>(v: V, v2: V, set: T) => void, thisArg?: any) {
|
||||
this.items.forEach((v) => f.call(thisArg, v, v, this));
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[V, V]> {
|
||||
return _iterMap(this.items.values(), (v) => [v, v]);
|
||||
}
|
||||
|
||||
keys(): IterableIterator<V> {
|
||||
return this.items.values();
|
||||
}
|
||||
|
||||
values(): IterableIterator<V> {
|
||||
return this.items.values();
|
||||
}
|
||||
|
||||
delete(v: V): boolean {
|
||||
return this.items.delete(this._key(v));
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.items.clear();
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.items.size;
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<V> {
|
||||
return this.items.values();
|
||||
}
|
||||
|
||||
[Symbol.toStringTag] = 'FlexSet';
|
||||
|
||||
equals(other: any): boolean {
|
||||
if (!('size' in other && 'has' in other)) return false;
|
||||
if (this.size !== other.size) return false;
|
||||
for (let v of this.items.values()) {
|
||||
if (!other.has(v)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
canonicalValues(): IterableIterator<string> {
|
||||
return this.items.keys();
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export * from './flex';
|
||||
export * from './symbols';
|
||||
export * from './codec';
|
||||
export * from './values';
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { Value } from './values';
|
||||
|
||||
export function stringify(x: Value): string {
|
||||
export function stringify(x: any): string {
|
||||
if (typeof x === 'object' && x !== null && 'asPreservesText' in x) {
|
||||
return x.asPreservesText();
|
||||
} else {
|
||||
return JSON.stringify(x);
|
||||
try {
|
||||
return JSON.stringify(x);
|
||||
} catch (_e) {
|
||||
return ('' + x).asPreservesText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,13 +3,15 @@
|
|||
import { PreserveOn, AsPreserve } from './symbols';
|
||||
import { Tag } from './constants';
|
||||
import { Encoder, encode } from './codec';
|
||||
import { stringify } from './text';
|
||||
import { _iterMap, FlexMap, FlexSet, IdentityMap, IdentitySet } from './flex';
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
export type Value = Atom | Compound | Annotated;
|
||||
export type Atom = boolean | Single | Double | number | string | Bytes | symbol;
|
||||
export type Compound = Record | Array<Value> | Set | Dictionary;
|
||||
export type Compound = Record | Array<Value> | Set | Dictionary<Value>;
|
||||
|
||||
export function isRecord(x: any): x is Record {
|
||||
return Array.isArray(x) && 'label' in x;
|
||||
|
@ -517,12 +519,14 @@ export interface RecordConstructorInfo {
|
|||
arity: number;
|
||||
}
|
||||
|
||||
export function is(a: Value, b: Value): boolean {
|
||||
export function is(a: any, b: any): boolean {
|
||||
if (isAnnotated(a)) a = a.item;
|
||||
if (isAnnotated(b)) b = b.item;
|
||||
if (Object.is(a, b)) return true;
|
||||
if (typeof a !== typeof b) return false;
|
||||
if (typeof a === 'object') {
|
||||
if (a === null || b === null) return false;
|
||||
if ('equals' in a) return a.equals(b);
|
||||
if ('equals' in a) return a.equals(b, is);
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) if (!is(a[i], b[i])) return false;
|
||||
|
@ -542,176 +546,119 @@ export function isClassOf(ci: RecordConstructorInfo, v: any): v is Record {
|
|||
|
||||
export type DictionaryType = 'Dictionary' | 'Set';
|
||||
|
||||
export function is_Dictionary(x: any, t: DictionaryType): x is _Dictionary {
|
||||
return typeof x === 'object' && x !== null &&
|
||||
'_items' in x &&
|
||||
'_dictionaryType' in x &&
|
||||
x._dictionaryType() === t;
|
||||
export function is_Dictionary(x: any, t: DictionaryType): boolean {
|
||||
return typeof x === 'object' && x !== null && x[Symbol.toStringTag] === t;
|
||||
}
|
||||
|
||||
export const isDictionary = (x: any): x is Dictionary => is_Dictionary(x, 'Dictionary');
|
||||
export const isDictionary = <T> (x: any): x is Dictionary<T> => is_Dictionary(x, 'Dictionary');
|
||||
export const isSet = (x: any): x is Set => is_Dictionary(x, 'Set');
|
||||
|
||||
export type DictionaryEntry = [Value, Value];
|
||||
export function _canonicalString(item: Value): string {
|
||||
const bs = encode(item, { canonical: true })._view;
|
||||
const s = String.fromCharCode.apply(null, bs);
|
||||
return s;
|
||||
}
|
||||
|
||||
export abstract class _Dictionary {
|
||||
_items: { [key: string]: DictionaryEntry } = {};
|
||||
|
||||
_key(key: Value): string {
|
||||
const bs = encode(key, { canonical: true })._view;
|
||||
const s = String.fromCharCode.apply(null, bs);
|
||||
return s;
|
||||
}
|
||||
|
||||
_lookup(key: Value): DictionaryEntry | null {
|
||||
const k = this._key(key);
|
||||
return k in this._items ? this._items[k] : null;
|
||||
}
|
||||
|
||||
_set(key: Value, value: Value) {
|
||||
this._items[this._key(key)] = [key, value];
|
||||
}
|
||||
|
||||
_get(key: Value, defaultValue?: Value): Value {
|
||||
const k = this._key(key);
|
||||
return k in this._items ? this._items[k][1] : defaultValue;
|
||||
}
|
||||
|
||||
_delete(key: Value) {
|
||||
delete this._items[this._key(key)];
|
||||
}
|
||||
|
||||
_forEach(f: (value: Value, key: Value) => void) {
|
||||
for (let ks in this._items) {
|
||||
const [k, v] = this._items[ks];
|
||||
f(v, k);
|
||||
export class Dictionary<T> extends FlexMap<Value, T> {
|
||||
static fromJS(x: object): Dictionary<Value> {
|
||||
if (isDictionary(x)) return x as Dictionary<Value>;
|
||||
const d = new Dictionary<Value>();
|
||||
for (let key in x) {
|
||||
const value = x[key];
|
||||
d.set(key, fromJS(value));
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
_map<T>(f: (value: Value, key: Value) => T): Array<T> {
|
||||
const result = [];
|
||||
for (let ks in this._items) {
|
||||
const [k, v] = this._items[ks];
|
||||
result.push(f(v, k));
|
||||
constructor(items?: Iterable<readonly [any, T]>) {
|
||||
super(_canonicalString, _iterMap(items?.[Symbol.iterator](), ([k,v]) => [fromJS(k), v]));
|
||||
}
|
||||
|
||||
mapEntries<R>(f: (entry: [Value, T]) => [Value, R]): Dictionary<R> {
|
||||
const result = new Dictionary<R>();
|
||||
for (let oldEntry of this.entries()) {
|
||||
const newEntry = f(oldEntry);
|
||||
result.set(newEntry[0], newEntry[1])
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
equals(other: any): boolean {
|
||||
if (!Object.is(other.constructor, this.constructor)) return false;
|
||||
const es1 = Object.entries(this._items);
|
||||
if (es1.length !== Object.entries(other._items).length) return false;
|
||||
for (let [ks1, e1] of es1) {
|
||||
const e2 = other._items[ks1];
|
||||
if (!is(e1[1], e2[1])) return false;
|
||||
}
|
||||
return true;
|
||||
asPreservesText(): string {
|
||||
return '{' +
|
||||
Array.from(_iterMap(this.entries(), ([k, v]) =>
|
||||
k.asPreservesText() + ': ' + stringify(v))).join(', ') +
|
||||
'}';
|
||||
}
|
||||
|
||||
clone(): Dictionary<T> {
|
||||
return new Dictionary(this);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.asPreservesText();
|
||||
}
|
||||
|
||||
abstract asPreservesText(): string;
|
||||
abstract _dictionaryType(): DictionaryType;
|
||||
}
|
||||
[Symbol.toStringTag] = 'Dictionary';
|
||||
|
||||
export class Dictionary extends _Dictionary {
|
||||
static fromJS(x: object): Dictionary {
|
||||
if (isDictionary(x)) return x;
|
||||
const d = new Dictionary();
|
||||
for (let key in x) {
|
||||
const value = x[key];
|
||||
d._set(key, fromJS(value));
|
||||
[PreserveOn](encoder: Encoder) {
|
||||
if (encoder.canonical) {
|
||||
const pieces = Array.from(this).map(([k, v]) =>
|
||||
Bytes.concat([encode(k, { canonical: true }),
|
||||
encode(v, { canonical: true })]));
|
||||
pieces.sort(Bytes.compare);
|
||||
encoder.encoderawvalues(Tag.Dictionary, pieces);
|
||||
} else {
|
||||
encoder.emitbyte(Tag.Dictionary);
|
||||
this.forEach((v, k) => {
|
||||
encoder.push(k);
|
||||
encoder.push(v);
|
||||
});
|
||||
encoder.emitbyte(Tag.End);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
_dictionaryType(): DictionaryType {
|
||||
return 'Dictionary';
|
||||
}
|
||||
|
||||
set(key: Value, value: Value) {
|
||||
this._set(key, value);
|
||||
}
|
||||
|
||||
get(key: Value, defaultValue?: Value): Value {
|
||||
return this._get(key, defaultValue);
|
||||
}
|
||||
|
||||
delete(key: Value) {
|
||||
this._delete(key);
|
||||
}
|
||||
|
||||
mapEntries(f: (entry: DictionaryEntry) => DictionaryEntry): Dictionary {
|
||||
const result = new Dictionary();
|
||||
for (let ks in this._items) {
|
||||
const oldEntry = this._items[ks];
|
||||
const newEntry = f(oldEntry);
|
||||
result._set(newEntry[0], newEntry[1])
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
forEach(f: (value: Value, key: Value) => void) {
|
||||
this._forEach(f);
|
||||
}
|
||||
|
||||
asPreservesText(): string {
|
||||
return '{' +
|
||||
this._map((v, k) => k.asPreservesText() + ': ' + v.asPreservesText()).join(', ') +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
export class Set extends _Dictionary implements Iterable<Value> {
|
||||
constructor(items: Iterable<any> = []) {
|
||||
super();
|
||||
for (let item of items) this.add(fromJS(item));
|
||||
}
|
||||
|
||||
_dictionaryType(): DictionaryType {
|
||||
return 'Set';
|
||||
}
|
||||
|
||||
add(v: Value) {
|
||||
this._set(v, true);
|
||||
}
|
||||
|
||||
delete(v: Value) {
|
||||
this._delete(v);
|
||||
}
|
||||
|
||||
includes(key: Value) {
|
||||
return this._lookup(key) !== null;
|
||||
}
|
||||
|
||||
forEach(f: (value: Value) => void) {
|
||||
this._forEach((_v, k) => f(k));
|
||||
export class Set extends FlexSet<Value> {
|
||||
constructor(items?: Iterable<any>) {
|
||||
super(_canonicalString, _iterMap(items?.[Symbol.iterator](), fromJS));
|
||||
}
|
||||
|
||||
map(f: (value: Value) => Value): Set {
|
||||
const result = new Set();
|
||||
for (let ks in this._items) {
|
||||
const k = this._items[ks][0];
|
||||
result._set(f(k), true);
|
||||
}
|
||||
return result;
|
||||
return new Set(_iterMap(this[Symbol.iterator](), f));
|
||||
}
|
||||
|
||||
[Symbol.iterator](): Iterator<Value> {
|
||||
return this._map((_v, k) => k)[Symbol.iterator]();
|
||||
filter(f: (value: Value) => boolean): Set {
|
||||
const result = new Set();
|
||||
for (let k of this) if (f(k)) result.add(k);
|
||||
return result;
|
||||
}
|
||||
|
||||
asPreservesText(): string {
|
||||
return '#{' +
|
||||
this._map((_v, k) => k.asPreservesText()).join(', ') +
|
||||
Array.from(_iterMap(this.values(), v => v.asPreservesText())).join(', ') +
|
||||
'}';
|
||||
}
|
||||
|
||||
clone(): Set {
|
||||
return new Set(this);
|
||||
}
|
||||
|
||||
[Symbol.toStringTag] = 'Set';
|
||||
|
||||
[PreserveOn](encoder: Encoder) {
|
||||
if (encoder.canonical) {
|
||||
const pieces = Array.from(this).map(k => encode(k, { canonical: true }));
|
||||
pieces.sort(Bytes.compare);
|
||||
encoder.encoderawvalues(Tag.Set, pieces);
|
||||
} else {
|
||||
encoder.encodevalues(Tag.Set, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isAnnotated(x: any): x is Annotated {
|
||||
return typeof x === 'object' && x !== null &&
|
||||
x.constructor.name === 'Annotated' &&
|
||||
'annotations' in x &&
|
||||
'item' in x;
|
||||
}
|
||||
|
@ -740,7 +687,7 @@ export class Annotated {
|
|||
}
|
||||
|
||||
equals(other: any): boolean {
|
||||
return isAnnotated(other) && is(this.item, other.item);
|
||||
return is(this.item, isAnnotated(other) ? other.item : other);
|
||||
}
|
||||
|
||||
hashCode(): number {
|
||||
|
|
|
@ -132,7 +132,7 @@ describe('common test suite', () => {
|
|||
});
|
||||
}
|
||||
|
||||
const tests = peel(TestCases._.cases(peel(samples))) as Dictionary;
|
||||
const tests = peel(TestCases._.cases(peel(samples))) as Dictionary<Value>;
|
||||
tests.forEach((t0: Value, tName0: Value) => {
|
||||
const tName = Symbol.keyFor(strip(tName0) as symbol);
|
||||
const t = peel(t0) as Record;
|
||||
|
|
Loading…
Reference in New Issue