Types: Separate shape from data

This commit is contained in:
Daniel Lehenbauer 2020-09-15 21:48:03 +00:00
Родитель 446c8c6956
Коммит a538266223
12 изменённых файлов: 267 добавлений и 83 удалений

Просмотреть файл

@ -1,3 +1,8 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { IProducer, IReader, IConsumer } from "@tiny-calc/types";
import { ConsumerSet, addConsumer, removeConsumer, forEachConsumer } from "../consumerset";
@ -6,12 +11,12 @@ export abstract class Producer<TMap> implements IProducer<TMap>, IReader<TMap> {
//#region IProducer
open(consumer: IConsumer<TMap>): IReader<TMap> {
public open(consumer: IConsumer<TMap>): IReader<TMap> {
this.consumers = addConsumer(this.consumers, consumer);
return this;
}
close(consumer: IConsumer<TMap>): void {
public close(consumer: IConsumer<TMap>): void {
this.consumers = removeConsumer(this.consumers, consumer);
}
@ -19,15 +24,15 @@ export abstract class Producer<TMap> implements IProducer<TMap>, IReader<TMap> {
//#region IReader
public abstract get<K extends keyof TMap>(property: K): TMap[K];
public abstract get<K extends keyof TMap>(key: K): TMap[K];
public get producer() { return this; }
public get producer():IProducer<TMap> { return this; }
//#endregion IReader
protected invalidateValue<K extends keyof TMap>(property: K): void {
protected invalidateValue<K extends keyof TMap>(key: K): void {
forEachConsumer(this.consumers, (consumer) => {
consumer.valueChanged(property, this);
consumer.keyChanged(key, this);
});
}
}

Просмотреть файл

@ -6,7 +6,14 @@
import "mocha";
import { strict as assert } from "assert";
import { Random } from "best-random";
import { IMatrixConsumer, IMatrixReader, IMatrixProducer, IVectorWriter, IMatrixWriter } from "@tiny-calc/types";
import {
IMatrixConsumer,
IMatrixReader,
IMatrixProducer,
IMatrixWriter,
IVectorShapeWriter,
IVectorWriter,
} from "@tiny-calc/types";
import { DenseVector, RowMajorMatrix } from "../src";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -95,8 +102,8 @@ export class TestMatrix<T = any, TRow = never, TCol = never> implements IMatrixC
public constructor (
producer: IMatrixProducer<T>,
private readonly rowWriter: IVectorWriter<TRow>,
private readonly colWriter: IVectorWriter<TCol>,
private readonly rowWriter: IVectorWriter<TRow> & IVectorShapeWriter,
private readonly colWriter: IVectorWriter<TCol> & IVectorShapeWriter,
private readonly cellWriter: IMatrixWriter<T>
) {
this.reader = producer.openMatrix(this);
@ -109,7 +116,7 @@ export class TestMatrix<T = any, TRow = never, TCol = never> implements IMatrixC
public rowsChanged(rowStart: number, removedCount: number, insertedCount: number): void {
const rowEnd = rowStart + removedCount;
assert(0 <= rowStart && rowStart <= rowEnd && rowEnd <= this.consumed.rowCount);
if (removedCount > 0) { this.consumed.removeRows(rowStart, removedCount); }
@ -154,7 +161,7 @@ export class TestMatrix<T = any, TRow = never, TCol = never> implements IMatrixC
const actual = this.reader.getCell(row, col);
const expected = this.expected.getCell(row, col);
assert.equal(actual, expected);
assert.equal(this.consumed.getCell(row, col), expected);
@ -173,7 +180,7 @@ export class TestMatrix<T = any, TRow = never, TCol = never> implements IMatrixC
this.expected.setCell(row, col, value);
this.cellWriter.setCell(row, col, value);
assert.equal(this.getCell(row, col), value,
`Writer.setCell(${row},${col}) must update matrix value.`);
}
@ -239,7 +246,7 @@ export class TestMatrix<T = any, TRow = never, TCol = never> implements IMatrixC
}
public extract(): ReadonlyArray<ReadonlyArray<T>> {
const m: T[][] = [];
const m: T[][] = [];
for (let r = 0; r < this.rowCount; r++) {
const row: T[] = [];
m.push(row);
@ -265,7 +272,7 @@ export class TestMatrix<T = any, TRow = never, TCol = never> implements IMatrixC
public expectSize(rowCount: number, colCount: number): void {
this.check();
assert.equal(this.rowCount, rowCount);
assert.equal(this.colCount, colCount);
}

Просмотреть файл

@ -6,7 +6,7 @@
import "mocha";
import { strict as assert } from "assert";
import { Random } from "best-random";
import { IVectorConsumer, IVectorReader, IVectorProducer, IVectorWriter } from "@tiny-calc/types";
import { IVectorConsumer, IVectorReader, IVectorProducer, IVectorWriter, IVectorShapeWriter } from "@tiny-calc/types";
import { DenseVector } from "../src";
export class TestVector<T> implements IVectorConsumer<T> {
@ -14,7 +14,7 @@ export class TestVector<T> implements IVectorConsumer<T> {
private readonly consumed: T[] = [];
private readonly expected: T[] = [];
public constructor(producer: IVectorProducer<T>, private readonly writer: IVectorWriter<T>) {
public constructor(producer: IVectorProducer<T>, private readonly writer: IVectorWriter<T> & IVectorShapeWriter) {
this.actual = producer.openVector(this);
for (let i = 0; i < this.actual.length; i++) {
@ -41,11 +41,11 @@ export class TestVector<T> implements IVectorConsumer<T> {
public itemsChanged(start: number, removedCount: number, insertedCount: number, producer: IVectorProducer<T>): void {
const inserted = [];
for (let i = start; insertedCount > 0; i++, insertedCount--) {
inserted.push(this.actual.getItem(i));
}
this.consumed.splice(start, removedCount, ...inserted);
}

Просмотреть файл

@ -3,7 +3,7 @@
* Licensed under the MIT License.
*/
import {
import {
IConsumer,
IProducer,
IVectorConsumer,
@ -25,33 +25,33 @@ export class LoggingConsumer<T> implements IConsumer<T>, IVectorConsumer<T>, IMa
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(producer as any)[idSym] = value;
}
private getProducerId(producer: AnyProducer): string {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (producer as any)[idSym] ?? "Error: Missing call to LoggingConsumer.setProducerId(..)";
}
// #region IConsumer<T>
public valueChanged<U extends T, K extends keyof U>(property: K, producer: IProducer<U>): void {
public keyChanged<U extends T, K extends keyof U>(property: K, producer: IProducer<U>): void {
this.log.push({ property, producer: this.getProducerId(producer) });
}
// #endregion IConsumer<T>
// #region IVectorConsumer<T>
public itemsChanged(start: number, removedCount: number, insertedCount: number, producer: IVectorProducer<T>): void {
this.log.push({ start, removedCount, insertedCount, producer: this.getProducerId(producer) });
}
// #endregion IVectorConsumer<T>
// #region IMatrixConsumer<T>
public rowsChanged(rowStart: number, removedCount: number, insertedCount: number, producer: IMatrixProducer<T>): void {
this.log.push({ rowStart, removedCount, insertedCount, producer: this.getProducerId(producer) });
}
public colsChanged(colStart: number, removedCount: number, insertedCount: number, producer: IMatrixProducer<T>): void {
this.log.push({ colStart, removedCount, insertedCount, producer: this.getProducerId(producer) });
}
public cellsChanged(rowStart: number, colStart: number, rowCount: number, colCount: number, producer: IMatrixProducer<T>): void {
this.log.push({ rowStart, colStart, rowCount, colCount, producer: this.getProducerId(producer) });
}

Просмотреть файл

@ -9,7 +9,7 @@ class NullConsumer<T> implements IConsumer<T>, IVectorConsumer<T>, IMatrixConsum
public rowsChanged(): void { }
public colsChanged(): void { }
public cellsChanged(): void { }
public valueChanged(): void { }
public keyChanged(): void { }
public itemsChanged(): void { }
}

Просмотреть файл

@ -9,5 +9,5 @@ const listFormula = new ListFormula(context, "IF(Time.Now = Time.Now, Math.Max(T
});
for (let i = 0; i < 10; i++) {
listFormula.valueChanged();
listFormula.keyChanged();
}

Просмотреть файл

@ -30,7 +30,7 @@ export class Context implements Producer {
open() {
const producer: Producer = this;
return {
return {
get: (key: string) => this.fields[key],
producer
};
@ -47,7 +47,7 @@ export class TimeProducer implements Producer {
open() {
const time = Date.now();
const producer: Producer = this;
return {
return {
get: () => time,
producer
};
@ -114,7 +114,7 @@ export class ListFormula implements FormulaHost {
}
}
valueChanged() {
keyChanged() {
console.time("recalc");
const scope = createCalcValue(this.scope);
const values = [];

Просмотреть файл

@ -3,9 +3,36 @@
* Licensed under the MIT License.
*/
export { IConsumer, IProducer, IReader, IWriter } from './record';
export { IVectorConsumer, IVectorProducer, IVectorReader, IVectorWriter } from './vector';
export { IMatrixConsumer, IMatrixProducer, IMatrixReader, IMatrixWriter } from './matrix';
export {
IConsumer,
IProducer,
IReader,
IShapeConsumer,
IShapeProducer,
IShapeReader,
IShapeWriter,
IWriter
} from './record';
export {
IVectorConsumer,
IVectorProducer,
IVectorReader,
IVectorShapeConsumer,
IVectorShapeProducer,
IVectorShapeReader,
IVectorShapeWriter,
IVectorWriter
} from './vector';
export {
IMatrixConsumer,
IMatrixProducer,
IMatrixReader,
IMatrixShapeConsumer,
IMatrixShapeProducer,
IMatrixShapeReader,
IMatrixShapeWriter,
IMatrixWriter
} from './matrix';
export {
ITreeShapeConsumer,
ITreeShapeProducer,

Просмотреть файл

@ -4,27 +4,80 @@
*/
/** An observable 2D numerically indexed collection. */
export interface IMatrixProducer<T> {
export interface IMatrixShapeProducer {
/**
* Acquire a reader for this matrix's values and implicitly subscribe the consumer
* to value change notifications.
*
*
* @param consumer - The consumer to be notified of matrix changes.
*/
openMatrix(consumer: IMatrixShapeConsumer): IMatrixShapeReader;
/**
* Unsubscribe the consumer from this matrix's change notifications.
*
* @param consumer - The consumer to unregister from the matrix.
*/
closeMatrix(consumer: IMatrixShapeConsumer): void;
}
/** A consumer of change notifications for a matrix. */
export interface IMatrixShapeConsumer {
/** Notification that rows have been inserted, removed, and/or replaced in the given matrix. */
rowsChanged(rowStart: number, removedCount: number, insertedCount: number, producer: IMatrixShapeProducer): void;
/** Notification that cols have been inserted, removed, and/or replaced in the given matrix. */
colsChanged(colStart: number, removedCount: number, insertedCount: number, producer: IMatrixShapeProducer): void;
}
/** Capability to read cells in a matrix. */
export interface IMatrixShapeReader {
readonly rowCount: number;
readonly colCount: number;
/**
* A reference to the underlying producer that provides values for this reader,
* or undefined if the producer is immutable.
*/
readonly matrixProducer?: IMatrixShapeProducer;
}
/** Capability to write cells in a matrix. */
export interface IMatrixShapeWriter {
spliceRows(rowStart: number, deleteCount: number, insertCount: number): void;
spliceCols(colStart: number, deleteCount: number, insertCount: number): void;
}
/** An observable 2D numerically indexed collection. */
export interface IMatrixProducer<T> extends IMatrixShapeProducer {
/**
* Acquire a reader for this matrix's values and implicitly subscribe the consumer
* to value change notifications.
*
* @param consumer - The consumer to be notified of matrix changes.
*/
openMatrix(consumer: IMatrixConsumer<T>): IMatrixReader<T>;
/**
* Unsubscribe the consumer from this matrix's change notifications.
*
*
* @param consumer - The consumer to unregister from the matrix.
*/
closeMatrix(consumer: IMatrixConsumer<T>): void;
}
/** A consumer of change notifications for a matrix. */
export interface IMatrixConsumer<T> extends IMatrixShapeConsumer {
/**
* Notification that a range of cells have been replaced in the given matrix. If the source
* matrix has the new cell values already in an array, it may optionally pass these to consumers
* as an optimization.
*/
cellsChanged(rowStart: number, colStart: number, rowCount: number, colCount: number, producer: IMatrixProducer<T>): void;
}
/** Capability to read cells in a matrix. */
export interface IMatrixReader<T> {
readonly rowCount: number;
readonly colCount: number;
export interface IMatrixReader<T> extends IMatrixShapeReader {
getCell(row: number, col: number): T;
/**
@ -39,22 +92,6 @@ export interface IMatrixWriter<T> {
setCell(row: number, col: number, value: T): void;
}
/** A consumer of change notifications for a matrix. */
export interface IMatrixConsumer<T> {
/** Notification that rows have been inserted, removed, and/or replaced in the given matrix. */
rowsChanged(rowStart: number, removedCount: number, insertedCount: number, producer: IMatrixProducer<T>): void;
/** Notification that cols have been inserted, removed, and/or replaced in the given matrix. */
colsChanged(colStart: number, removedCount: number, insertedCount: number, producer: IMatrixProducer<T>): void;
/**
* Notification that a range of cells have been replaced in the given matrix. If the source
* matrix has the new cell values already in an array, it may optionally pass these to consumers
* as an optimization.
*/
cellsChanged(rowStart: number, colStart: number, rowCount: number, colCount: number, producer: IMatrixProducer<T>): void;
}
export interface MatrixIteratorSpec {
/**
* Iterates over empty cells.

Просмотреть файл

@ -3,45 +3,85 @@
* Licensed under the MIT License.
*/
/** An observable key/value collection, such as a Record or Map. */
export interface IShapeProducer<TKey> {
/**
* Acquire a reader for this producer's values and implicitly subscribe the consumer
* to value change notifications.
*
* @param consumer - The consumer to be notified of value changes.
*/
open(consumer: IShapeConsumer<TKey>): IShapeReader<TKey>;
/**
* Unsubscribe the given 'consumer' from this producer's change notifications.
*
* @param consumer - The consumer to unregister from the producer.
*/
close(consumer: IShapeConsumer<TKey>): void;
}
/** A consumer of change notifications for a key/value collection, such as a Record or Map. */
export interface IShapeConsumer<TKey> {
keyChanged(key: TKey, producer: IShapeProducer<TKey>): void;
}
/** Capability to read values of a key/value collection. */
export interface IShapeReader<TKey> {
keys(): IterableIterator<TKey>;
has(key: TKey): boolean;
readonly size: number;
/**
* A reference to the underlying producer that provides the shape for this reader,
* or undefined if the producer is immutable.
*/
readonly producer?: IShapeProducer<TKey>;
}
export interface IShapeWriter<TKey> {
delete(key: TKey): void;
clear(): void;
}
/** An observable key/value collection, such as a Record or Map. */
export interface IProducer<T> {
/**
* Acquire a reader for this producer's values and implicitly subscribe the consumer
* to value change notifications.
*
*
* @param consumer - The consumer to be notified of value changes.
*/
open(consumer: IConsumer<T>): IReader<T>;
open(consumer: IConsumer<T>): IReader<T> | (IReader<T> & IShapeReader<T>);
/**
* Unsubscribe the given 'consumer' from this producer's change notifications.
*
*
* @param consumer - The consumer to unregister from the producer.
*/
close(consumer: IConsumer<T>): void;
}
/** A consumer of change notifications for a key/value collection, such as a Record or Map. */
export interface IConsumer<T> {
keyChanged<K extends keyof T>(key: K, producer: IProducer<T>): void;
}
/** Capability to read values of a key/value collection. */
export interface IReader<T> {
/** Return the value associated with `property`. */
get<K extends keyof T>(property: K): T[K];
/** Return the value associated with `key`. */
get<K extends keyof T>(key: K): T[K];
/**
* A reference to the underlying producer that provides values for this reader,
* or undefined if the producer is immutable.
*/
readonly producer?: IProducer<T>;
readonly producer?: IProducer<T> | (IProducer<T> & IShapeProducer<keyof T>);
}
/** Capability to set values of a key/value collection. */
export interface IWriter<T> {
set<K extends keyof T>(property: K, value: T[K]): void;
}
/** A consumer of change notifications for a key/value collection, such as a Record or Map. */
export interface IConsumer<T> {
/**
* Invoked whenever the data this object is bound to is changed.
*/
valueChanged<U extends T, K extends keyof U>(property: K, producer: IProducer<U>): void;
set<K extends keyof T>(key: K, value: T[K]): void;
}

Просмотреть файл

@ -4,26 +4,64 @@
*/
/** An observable 1D numerically indexed collection, such as an Array. */
export interface IVectorProducer<T> {
export interface IVectorShapeProducer {
/**
* Acquire a reader for this vector's values and implicitly subscribe the consumer
* to value change notifications.
*
*
* @param consumer - The consumer to be notified of vector changes.
*/
openVector(consumer: IVectorShapeConsumer): IVectorShapeReader;
/**
* Unsubscribe the given 'consumer' from this vector's change notifications.
*
* @param consumer - The consumer to unregister from the vector.
*/
closeVector(consumer: IVectorShapeConsumer): void;
}
/** Capability to read items in a vector. */
export interface IVectorShapeReader {
readonly length: number;
/**
* A reference to the underlying producer that provides values for this reader,
* or undefined if the producer is immutable.
*/
readonly vectorProducer?: IVectorShapeProducer;
}
/** Capability to insert, replace, and remove items in a vector. */
export interface IVectorShapeWriter {
splice(start: number, deleteCount: number, insertCount: number): void;
}
export interface IVectorShapeConsumer {
/** Notification that a range of items have been inserted, removed, and/or replaced in the given vector. */
itemsChanged(start: number, removedCount: number, insertedCount: number, producer: IVectorShapeProducer): void;
}
/** An observable 1D numerically indexed collection, such as an Array. */
export interface IVectorProducer<T> extends IVectorShapeProducer {
/**
* Acquire a reader for this vector's values and implicitly subscribe the consumer
* to value change notifications.
*
* @param consumer - The consumer to be notified of vector changes.
*/
openVector(consumer: IVectorConsumer<T>): IVectorReader<T>;
/**
* Unsubscribe the given 'consumer' from this vector's change notifications.
*
*
* @param consumer - The consumer to unregister from the vector.
*/
closeVector(consumer: IVectorConsumer<T>): void;
}
/** Capability to read items in a vector. */
export interface IVectorReader<T> {
readonly length: number;
export interface IVectorReader<T> extends IVectorShapeReader {
getItem(index: number): T;
/**
@ -35,12 +73,11 @@ export interface IVectorReader<T> {
/** Capability to insert, replace, and remove items in a vector. */
export interface IVectorWriter<T> {
splice(start: number, deleteCount: number, insertCount: number): void;
setItem(index: number, item: T): void;
}
/** A consumer of change notifications for a vector. */
export interface IVectorConsumer<T> {
export interface IVectorConsumer<T> extends IVectorShapeConsumer {
/** Notification that a range of items have been inserted, removed, and/or replaced in the given vector. */
itemsChanged(start: number, removedCount: number, insertedCount: number, producer: IVectorProducer<T>): void;
}

Просмотреть файл

@ -5,32 +5,63 @@
import "mocha";
import { strict as assert } from "assert";
import { IReader, IWriter } from "../src";
import { IReader, IWriter, IShapeReader, IShapeWriter } from "../src";
describe("Record", () => {
describe("must be compatible with ES6 map", () => {
let map: Map<string, number>;
let shapeReader: IShapeReader<string>;
let shapeWriter: IShapeWriter<string>;
let reader: IReader<Record<string, number | undefined>>;
let writer: IWriter<Record<string, number>>;
beforeEach(() => {
map = new Map<string, number>();
shapeReader = map;
shapeWriter = map;
reader = map;
writer = map;
});
it("ES6 map must satisfy IReader", () => {
map.set("0", 0);
assert.equal(reader.get("0"), 0);
});
it("ES6 map must satisfy IWriter", () => {
writer.set("0", 0);
assert.equal(map.get("0"), 0);
});
it("ES6 map must satisfy IShapeReader", () => {
assert.deepEqual([...shapeReader.keys()], []);
const keys = ["a", "b", "c"];
for (const key of keys) {
assert.equal(shapeReader.has(key), false);
map.set(key, key.charCodeAt(0));
assert.equal(shapeReader.has(key), true);
}
assert.deepEqual([...shapeReader.keys()], keys);
});
it("ES6 map must satisfy IShapeWriter", () => {
const keys = ["a", "b", "c"];
for (const key of keys) {
assert.equal(shapeReader.has(key), false);
assert.equal(shapeWriter.delete(key), false);
map.set(key, key.charCodeAt(0));
assert.equal(shapeReader.has(key), true);
assert.equal(shapeWriter.delete(key), true);
assert.equal(shapeReader.has(key), false);
}
});
it("Must support key constraints", () => {
const map = new Map([
const map = new Map<"0" | "1", number>([
["0", 0],
["1", 1],
]);
@ -39,7 +70,7 @@ describe("Record", () => {
"0": number | undefined,
"1": number | undefined,
}> = map;
assert.equal(reader.get("0"), 0);
assert.equal(reader.get("1"), 1);