Organise data frame and vectors code

This commit is contained in:
Dominik Prokop
2019-09-16 16:53:06 +02:00
parent e9f1e86c8e
commit 843af51cc6
35 changed files with 750 additions and 711 deletions
@@ -0,0 +1,22 @@
import { MutableDataFrame } from './MutableDataFrame';
import { CircularVector } from '../vector/CircularVector';
interface CircularOptions {
append?: 'head' | 'tail';
capacity?: number;
}
/**
* This dataframe can have values constantly added, and will never
* exceed the given capacity
*/
export class CircularDataFrame<T = any> extends MutableDataFrame<T> {
constructor(options: CircularOptions) {
super(undefined, (buffer?: any[]) => {
return new CircularVector({
...options,
buffer,
});
});
}
}
@@ -1,7 +1,7 @@
import { FieldType, DataFrameDTO } from '../types/index';
import { MutableDataFrame } from './dataFrameHelper';
import { DataFrameView } from './dataFrameView';
import { DateTime } from './moment_wrapper';
import { FieldType, DataFrameDTO } from '../types/dataFrame';
import { DateTime } from '../utils/moment_wrapper';
import { MutableDataFrame } from './MutableDataFrame';
import { DataFrameView } from './DataFrameView';
interface MySpecialObject {
time: DateTime;
@@ -0,0 +1,78 @@
import { Vector } from '../types/vector';
import { DataFrame } from '../types/dataFrame';
/**
* This abstraction will present the contents of a DataFrame as if
* it were a well typed javascript object Vector.
*
* NOTE: The contents of the object returned from `view.get(index)`
* are optimized for use in a loop. All calls return the same object
* but the index has changed.
*
* For example, the three objects:
* const first = view.get(0);
* const second = view.get(1);
* const third = view.get(2);
* will point to the contents at index 2
*
* If you need three different objects, consider something like:
* const first = { ... view.get(0) };
* const second = { ... view.get(1) };
* const third = { ... view.get(2) };
*/
export class DataFrameView<T = any> implements Vector<T> {
private index = 0;
private obj: T;
constructor(private data: DataFrame) {
const obj = ({} as unknown) as T;
for (let i = 0; i < data.fields.length; i++) {
const field = data.fields[i];
const getter = () => {
return field.values.get(this.index);
};
if (!(obj as any).hasOwnProperty(field.name)) {
Object.defineProperty(obj, field.name, {
enumerable: true, // Shows up as enumerable property
get: getter,
});
}
Object.defineProperty(obj, i, {
enumerable: false, // Don't enumerate array index
get: getter,
});
}
this.obj = obj;
}
get dataFrame() {
return this.data;
}
get length() {
return this.data.length;
}
get(idx: number) {
this.index = idx;
return this.obj;
}
toArray(): T[] {
const arr: T[] = [];
for (let i = 0; i < this.data.length; i++) {
arr.push({ ...this.get(i) });
}
return arr;
}
toJSON(): T[] {
return this.toArray();
}
forEachRow(iterator: (row: T) => void) {
for (let i = 0; i < this.data.length; i++) {
iterator(this.get(i));
}
}
}
@@ -1,29 +1,6 @@
import { DataFrameDTO, FieldType } from '../types';
import { FieldCache, MutableDataFrame } from './dataFrameHelper';
import { toDataFrame } from './processDataFrame';
describe('dataFrameHelper', () => {
const frame = toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
{ name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] },
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
{ name: 'value', type: FieldType.number, values: [4, 5, 6] },
],
});
const ext = new FieldCache(frame);
it('should get the first field with a duplicate name', () => {
const field = ext.getFieldByName('value');
expect(field!.name).toEqual('value');
expect(field!.values.toJSON()).toEqual([1, 2, 3]);
});
it('should return index of the field', () => {
const field = ext.getFirstFieldOfType(FieldType.number);
expect(field!.index).toEqual(2);
});
});
import { FieldCache } from './FieldCache';
import { toDataFrame } from '../utils';
import { FieldType } from '../types/dataFrame';
describe('FieldCache', () => {
it('when creating a new FieldCache from fields should be able to query cache', () => {
@@ -90,68 +67,27 @@ describe('FieldCache', () => {
expect(fieldCache.getFieldByName('undefined')!.name).toEqual(expectedFieldNames[5]);
expect(fieldCache.getFieldByName('null')).toBeUndefined();
});
});
describe('reverse', () => {
describe('when called with a DataFrame', () => {
it('then it should reverse the order of values in all fields', () => {
const frame: DataFrameDTO = {
fields: [
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
{ name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] },
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
],
};
describe('field retrieval', () => {
const frame = toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
{ name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] },
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
{ name: 'value', type: FieldType.number, values: [4, 5, 6] },
],
});
const ext = new FieldCache(frame);
const helper = new MutableDataFrame(frame);
it('should get the first field with a duplicate name', () => {
const field = ext.getFieldByName('value');
expect(field!.name).toEqual('value');
expect(field!.values.toJSON()).toEqual([1, 2, 3]);
});
expect(helper.values.time.toArray()).toEqual([100, 200, 300]);
expect(helper.values.name.toArray()).toEqual(['a', 'b', 'c']);
expect(helper.values.value.toArray()).toEqual([1, 2, 3]);
helper.reverse();
expect(helper.values.time.toArray()).toEqual([300, 200, 100]);
expect(helper.values.name.toArray()).toEqual(['c', 'b', 'a']);
expect(helper.values.value.toArray()).toEqual([3, 2, 1]);
it('should return index of the field', () => {
const field = ext.getFirstFieldOfType(FieldType.number);
expect(field!.index).toEqual(2);
});
});
});
describe('Apending DataFrame', () => {
it('Should append values', () => {
const dto: DataFrameDTO = {
fields: [
{ name: 'time', type: FieldType.time, values: [100] },
{ name: 'name', type: FieldType.string, values: ['a', 'b'] },
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
],
};
const frame = new MutableDataFrame(dto);
expect(frame.values.time.toArray()).toEqual([100, null, null]);
// Set a value on the second row
frame.set(1, { time: 200, name: 'BB', value: 20 });
expect(frame.toArray()).toEqual([
{ time: 100, name: 'a', value: 1 }, // 1
{ time: 200, name: 'BB', value: 20 }, // 2
{ time: null, name: null, value: 3 }, // 3
]);
// Set a value on the second row
frame.add({ value2: 'XXX' }, true);
expect(frame.toArray()).toEqual([
{ time: 100, name: 'a', value: 1, value2: null }, // 1
{ time: 200, name: 'BB', value: 20, value2: null }, // 2
{ time: null, name: null, value: 3, value2: null }, // 3
{ time: null, name: null, value: null, value2: 'XXX' }, // 4
]);
// Make sure length survives a spread operator
const keys = Object.keys(frame);
const copy = { ...frame } as any;
expect(keys).toContain('length');
expect(copy.length).toEqual(frame.length);
});
});
@@ -0,0 +1,78 @@
import { Field, DataFrame, FieldType, guessFieldTypeForField } from '../index';
interface FieldWithIndex extends Field {
index: number;
}
export class FieldCache {
fields: FieldWithIndex[] = [];
private fieldByName: { [key: string]: FieldWithIndex } = {};
private fieldByType: { [key: string]: FieldWithIndex[] } = {};
constructor(data: DataFrame) {
this.fields = data.fields.map((field, idx) => ({
...field,
index: idx,
}));
for (let i = 0; i < data.fields.length; i++) {
const field = data.fields[i];
// Make sure it has a type
if (field.type === FieldType.other) {
const t = guessFieldTypeForField(field);
if (t) {
field.type = t;
}
}
if (!this.fieldByType[field.type]) {
this.fieldByType[field.type] = [];
}
this.fieldByType[field.type].push({
...field,
index: i,
});
if (this.fieldByName[field.name]) {
console.warn('Duplicate field names in DataFrame: ', field.name);
} else {
this.fieldByName[field.name] = { ...field, index: i };
}
}
}
getFields(type?: FieldType): FieldWithIndex[] {
if (!type) {
return [...this.fields]; // All fields
}
const fields = this.fieldByType[type];
if (fields) {
return [...fields];
}
return [];
}
hasFieldOfType(type: FieldType): boolean {
const types = this.fieldByType[type];
return types && types.length > 0;
}
getFirstFieldOfType(type: FieldType): FieldWithIndex | undefined {
const arr = this.fieldByType[type];
if (arr && arr.length > 0) {
return arr[0];
}
return undefined;
}
hasFieldNamed(name: string): boolean {
return !!this.fieldByName[name];
}
/**
* Returns the first field with the given name.
*/
getFieldByName(name: string): FieldWithIndex | undefined {
return this.fieldByName[name];
}
}
@@ -0,0 +1,66 @@
import { DataFrameDTO, FieldType } from '../types/dataFrame';
import { MutableDataFrame } from './MutableDataFrame';
describe('Reversing DataFrame', () => {
describe('when called with a DataFrame', () => {
it('then it should reverse the order of values in all fields', () => {
const frame: DataFrameDTO = {
fields: [
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
{ name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] },
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
],
};
const helper = new MutableDataFrame(frame);
expect(helper.values.time.toArray()).toEqual([100, 200, 300]);
expect(helper.values.name.toArray()).toEqual(['a', 'b', 'c']);
expect(helper.values.value.toArray()).toEqual([1, 2, 3]);
helper.reverse();
expect(helper.values.time.toArray()).toEqual([300, 200, 100]);
expect(helper.values.name.toArray()).toEqual(['c', 'b', 'a']);
expect(helper.values.value.toArray()).toEqual([3, 2, 1]);
});
});
});
describe('Apending DataFrame', () => {
it('Should append values', () => {
const dto: DataFrameDTO = {
fields: [
{ name: 'time', type: FieldType.time, values: [100] },
{ name: 'name', type: FieldType.string, values: ['a', 'b'] },
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
],
};
const frame = new MutableDataFrame(dto);
expect(frame.values.time.toArray()).toEqual([100, null, null]);
// Set a value on the second row
frame.set(1, { time: 200, name: 'BB', value: 20 });
expect(frame.toArray()).toEqual([
{ time: 100, name: 'a', value: 1 }, // 1
{ time: 200, name: 'BB', value: 20 }, // 2
{ time: null, name: null, value: 3 }, // 3
]);
// Set a value on the second row
frame.add({ value2: 'XXX' }, true);
expect(frame.toArray()).toEqual([
{ time: 100, name: 'a', value: 1, value2: null }, // 1
{ time: 200, name: 'BB', value: 20, value2: null }, // 2
{ time: null, name: null, value: 3, value2: null }, // 3
{ time: null, name: null, value: null, value2: 'XXX' }, // 4
]);
// Make sure length survives a spread operator
const keys = Object.keys(frame);
const copy = { ...frame } as any;
expect(keys).toContain('length');
expect(copy.length).toEqual(frame.length);
});
});
@@ -1,111 +1,12 @@
import { Field, FieldType, DataFrame, Vector, FieldDTO, DataFrameDTO } from '../types/dataFrame';
import { Labels, QueryResultMeta, KeyValue } from '../types/data';
import { guessFieldTypeForField, guessFieldTypeFromValue, toDataFrameDTO } from './processDataFrame';
import { ArrayVector, MutableVector, vectorToArray, CircularVector } from './vector';
import { Field, DataFrame, DataFrameDTO, FieldDTO, FieldType } from '../types/dataFrame';
import { KeyValue, QueryResultMeta, Labels } from '../types/data';
import { guessFieldTypeFromValue, guessFieldTypeForField, toDataFrameDTO } from '../utils/processDataFrame';
import isArray from 'lodash/isArray';
import isString from 'lodash/isString';
interface FieldWithIndex extends Field {
index: number;
}
export class FieldCache {
fields: FieldWithIndex[] = [];
private fieldByName: { [key: string]: FieldWithIndex } = {};
private fieldByType: { [key: string]: FieldWithIndex[] } = {};
constructor(data: DataFrame) {
this.fields = data.fields.map((field, idx) => ({
...field,
index: idx,
}));
for (let i = 0; i < data.fields.length; i++) {
const field = data.fields[i];
// Make sure it has a type
if (field.type === FieldType.other) {
const t = guessFieldTypeForField(field);
if (t) {
field.type = t;
}
}
if (!this.fieldByType[field.type]) {
this.fieldByType[field.type] = [];
}
this.fieldByType[field.type].push({
...field,
index: i,
});
if (this.fieldByName[field.name]) {
console.warn('Duplicate field names in DataFrame: ', field.name);
} else {
this.fieldByName[field.name] = { ...field, index: i };
}
}
}
getFields(type?: FieldType): FieldWithIndex[] {
if (!type) {
return [...this.fields]; // All fields
}
const fields = this.fieldByType[type];
if (fields) {
return [...fields];
}
return [];
}
hasFieldOfType(type: FieldType): boolean {
const types = this.fieldByType[type];
return types && types.length > 0;
}
getFirstFieldOfType(type: FieldType): FieldWithIndex | undefined {
const arr = this.fieldByType[type];
if (arr && arr.length > 0) {
return arr[0];
}
return undefined;
}
hasFieldNamed(name: string): boolean {
return !!this.fieldByName[name];
}
/**
* Returns the first field with the given name.
*/
getFieldByName(name: string): FieldWithIndex | undefined {
return this.fieldByName[name];
}
}
function makeFieldParser(value: any, field: Field): (value: string) => any {
if (!field.type) {
if (field.name === 'time' || field.name === 'Time') {
field.type = FieldType.time;
} else {
field.type = guessFieldTypeFromValue(value);
}
}
if (field.type === FieldType.number) {
return (value: string) => {
return parseFloat(value);
};
}
// Will convert anything that starts with "T" to true
if (field.type === FieldType.boolean) {
return (value: string) => {
return !(value[0] === 'F' || value[0] === 'f' || value[0] === '0');
};
}
// Just pass the string back
return (value: string) => value;
}
import { makeFieldParser } from '../utils/fieldParser';
import { MutableVector, Vector } from '../types/vector';
import { ArrayVector } from '../vector/ArrayVector';
import { vectorToArray } from '../vector/vectorToArray';
export type MutableField<T = any> = Field<T, MutableVector<T>>;
@@ -380,23 +281,3 @@ export class MutableDataFrame<T = any> implements DataFrame, MutableVector<T> {
return toDataFrameDTO(this);
}
}
interface CircularOptions {
append?: 'head' | 'tail';
capacity?: number;
}
/**
* This dataframe can have values constantly added, and will never
* exceed the given capacity
*/
export class CircularDataFrame<T = any> extends MutableDataFrame<T> {
constructor(options: CircularOptions) {
super(undefined, (buffer?: any[]) => {
return new CircularVector({
...options,
buffer,
});
});
}
}
@@ -0,0 +1,4 @@
export * from './DataFrameView';
export * from './FieldCache';
export * from './CircularDataFrame';
export * from './MutableDataFrame';
+2
View File
@@ -1,2 +1,4 @@
export * from './utils/index';
export * from './types/index';
export * from './vector';
export * from './dataframe';
+1 -19
View File
@@ -4,6 +4,7 @@ import { QueryResultBase, Labels, NullValueMode } from './data';
import { FieldCalcs } from '../utils/index';
import { DisplayProcessor } from './displayValue';
import { DataLink } from './dataLink';
import { Vector } from './vector';
export enum FieldType {
time = 'time', // or date
@@ -44,25 +45,6 @@ export interface FieldConfig {
noValue?: string;
}
export interface Vector<T = any> {
length: number;
/**
* Access the value by index (Like an array)
*/
get(index: number): T;
/**
* Get the resutls as an array.
*/
toArray(): T[];
/**
* Return the values as a simple array for json serialization
*/
toJSON(): any; // same results as toArray()
}
export interface Field<T = any, V = Vector<T>> {
name: string; // The column name
type: FieldType;
+40
View File
@@ -0,0 +1,40 @@
export interface Vector<T = any> {
length: number;
/**
* Access the value by index (Like an array)
*/
get(index: number): T;
/**
* Get the resutls as an array.
*/
toArray(): T[];
/**
* Return the values as a simple array for json serialization
*/
toJSON(): any; // same results as toArray()
}
/**
* Apache arrow vectors are Read/Write
*/
export interface ReadWriteVector<T = any> extends Vector<T> {
set: (index: number, value: T) => void;
}
/**
* Vector with standard manipulation functions
*/
export interface MutableVector<T = any> extends ReadWriteVector<T> {
/**
* Adds the value to the vector
*/
add: (value: T) => void;
/**
* modifies the vector so it is now the oposite order
*/
reverse: () => void;
}
+1 -1
View File
@@ -6,7 +6,7 @@ import isNumber from 'lodash/isNumber';
// Types
import { DataFrame, Field, FieldType, FieldConfig } from '../types';
import { guessFieldTypeFromValue } from './processDataFrame';
import { MutableDataFrame } from './dataFrameHelper';
import { MutableDataFrame } from '../dataframe/MutableDataFrame';
export enum CSVHeaderStyle {
full,
@@ -1,77 +0,0 @@
import { DataFrame, Vector } from '../types/index';
/**
* This abstraction will present the contents of a DataFrame as if
* it were a well typed javascript object Vector.
*
* NOTE: The contents of the object returned from `view.get(index)`
* are optimized for use in a loop. All calls return the same object
* but the index has changed.
*
* For example, the three objects:
* const first = view.get(0);
* const second = view.get(1);
* const third = view.get(2);
* will point to the contents at index 2
*
* If you need three different objects, consider something like:
* const first = { ... view.get(0) };
* const second = { ... view.get(1) };
* const third = { ... view.get(2) };
*/
export class DataFrameView<T = any> implements Vector<T> {
private index = 0;
private obj: T;
constructor(private data: DataFrame) {
const obj = ({} as unknown) as T;
for (let i = 0; i < data.fields.length; i++) {
const field = data.fields[i];
const getter = () => {
return field.values.get(this.index);
};
if (!(obj as any).hasOwnProperty(field.name)) {
Object.defineProperty(obj, field.name, {
enumerable: true, // Shows up as enumerable property
get: getter,
});
}
Object.defineProperty(obj, i, {
enumerable: false, // Don't enumerate array index
get: getter,
});
}
this.obj = obj;
}
get dataFrame() {
return this.data;
}
get length() {
return this.data.length;
}
get(idx: number) {
this.index = idx;
return this.obj;
}
toArray(): T[] {
const arr: T[] = [];
for (let i = 0; i < this.data.length; i++) {
arr.push({ ...this.get(i) });
}
return arr;
}
toJSON(): T[] {
return this.toArray();
}
forEachRow(iterator: (row: T) => void) {
for (let i = 0; i < this.data.length; i++) {
iterator(this.get(i));
}
}
}
@@ -0,0 +1,28 @@
import { Field, FieldType } from '../types/dataFrame';
import { guessFieldTypeFromValue } from './processDataFrame';
export function makeFieldParser(value: any, field: Field): (value: string) => any {
if (!field.type) {
if (field.name === 'time' || field.name === 'Time') {
field.type = FieldType.time;
} else {
field.type = guessFieldTypeFromValue(value);
}
}
if (field.type === FieldType.number) {
return (value: string) => {
return parseFloat(value);
};
}
// Will convert anything that starts with "T" to true
if (field.type === FieldType.boolean) {
return (value: string) => {
return !(value[0] === 'F' || value[0] === 'f' || value[0] === '0');
};
}
// Just pass the string back
return (value: string) => value;
}
@@ -3,9 +3,9 @@ import difference from 'lodash/difference';
import { fieldReducers, ReducerID, reduceField } from './fieldReducer';
import { Field, FieldType } from '../types/index';
import { MutableDataFrame } from './dataFrameHelper';
import { ArrayVector } from './vector';
import { guessFieldTypeFromValue } from './processDataFrame';
import { MutableDataFrame } from '../dataframe/MutableDataFrame';
import { ArrayVector } from '../vector/ArrayVector';
/**
* Run a reducer and get back the value
-3
View File
@@ -12,9 +12,6 @@ export * from './object';
export * from './moment_wrapper';
export * from './thresholds';
export * from './text';
export * from './dataFrameHelper';
export * from './dataFrameView';
export * from './vector';
export { getMappedValue } from './valueMappings';
+1 -1
View File
@@ -2,7 +2,7 @@ import { countBy, chain, map, escapeRegExp } from 'lodash';
import { LogLevel, LogRowModel, LogLabelStatsModel, LogsParser } from '../types/logs';
import { DataFrame, FieldType } from '../types/index';
import { ArrayVector } from './vector';
import { ArrayVector } from '../vector/ArrayVector';
const LOGFMT_REGEXP = /(?:^|\s)(\w+)=("[^"]*"|\S+)/;
@@ -9,7 +9,7 @@ import {
} from './processDataFrame';
import { FieldType, TimeSeries, TableData, DataFrameDTO } from '../types/index';
import { dateTime } from './moment_wrapper';
import { MutableDataFrame } from './dataFrameHelper';
import { MutableDataFrame } from '../dataframe/MutableDataFrame';
describe('toDataFrame', () => {
it('converts timeseries to series', () => {
@@ -18,9 +18,10 @@ import {
DataFrameDTO,
} from '../types/index';
import { isDateTime } from './moment_wrapper';
import { ArrayVector, SortedVector } from './vector';
import { MutableDataFrame } from './dataFrameHelper';
import { deprecationWarning } from './deprecationWarning';
import { ArrayVector } from '../vector/ArrayVector';
import { MutableDataFrame } from '../dataframe/MutableDataFrame';
import { SortedVector } from '../vector/SortedVector';
function convertTableToDataFrame(table: TableData): DataFrame {
const fields = table.columns.map(c => {
@@ -1,7 +1,7 @@
import { DataTransformerInfo } from './transformers';
import { DataFrame } from '../../types/dataFrame';
import { DataTransformerID } from './ids';
import { MutableDataFrame } from '../dataFrameHelper';
import { MutableDataFrame } from '../../dataframe/MutableDataFrame';
export interface AppendOptions {}
@@ -5,8 +5,8 @@ import { alwaysFieldMatcher } from '../matchers/predicates';
import { DataTransformerID } from './ids';
import { ReducerID, fieldReducers, reduceField } from '../fieldReducer';
import { KeyValue } from '../../types/data';
import { ArrayVector } from '../vector';
import { guessFieldTypeForField } from '../processDataFrame';
import { ArrayVector } from '../../vector/ArrayVector';
export interface ReduceTransformerOptions {
reducers: ReducerID[];
@@ -2,7 +2,7 @@ import { DataTransformerID } from './ids';
import { dataTransformers } from './transformers';
import { toDataFrame } from '../processDataFrame';
import { ReducerID } from '../fieldReducer';
import { DataFrameView } from '../dataFrameView';
import { DataFrameView } from '../../dataframe/DataFrameView';
describe('Transformers', () => {
it('should load all transformeres', () => {
-338
View File
@@ -1,338 +0,0 @@
import { Vector } from '../types/dataFrame';
export function vectorToArray<T>(v: Vector<T>): T[] {
const arr: T[] = [];
for (let i = 0; i < v.length; i++) {
arr[i] = v.get(i);
}
return arr;
}
/**
* Apache arrow vectors are Read/Write
*/
export interface ReadWriteVector<T = any> extends Vector<T> {
set: (index: number, value: T) => void;
}
/**
* Vector with standard manipulation functions
*/
export interface MutableVector<T = any> extends ReadWriteVector<T> {
/**
* Adds the value to the vector
*/
add: (value: T) => void;
/**
* modifies the vector so it is now the oposite order
*/
reverse: () => void;
}
export class ArrayVector<T = any> implements MutableVector<T> {
buffer: T[];
constructor(buffer?: T[]) {
this.buffer = buffer ? buffer : [];
}
get length() {
return this.buffer.length;
}
add(value: T) {
this.buffer.push(value);
}
get(index: number): T {
return this.buffer[index];
}
set(index: number, value: T) {
this.buffer[index] = value;
}
reverse() {
this.buffer.reverse();
}
toArray(): T[] {
return this.buffer;
}
toJSON(): T[] {
return this.buffer;
}
}
export class ConstantVector<T = any> implements Vector<T> {
constructor(private value: T, private len: number) {}
get length() {
return this.len;
}
get(index: number): T {
return this.value;
}
toArray(): T[] {
const arr = new Array<T>(this.length);
return arr.fill(this.value);
}
toJSON(): T[] {
return this.toArray();
}
}
export class ScaledVector implements Vector<number> {
constructor(private source: Vector<number>, private scale: number) {}
get length(): number {
return this.source.length;
}
get(index: number): number {
return this.source.get(index) * this.scale;
}
toArray(): number[] {
return vectorToArray(this);
}
toJSON(): number[] {
return vectorToArray(this);
}
}
/**
* Values are returned in the order defined by the input parameter
*/
export class SortedVector<T = any> implements Vector<T> {
constructor(private source: Vector<T>, private order: number[]) {}
get length(): number {
return this.source.length;
}
get(index: number): T {
return this.source.get(this.order[index]);
}
toArray(): T[] {
return vectorToArray(this);
}
toJSON(): T[] {
return vectorToArray(this);
}
}
interface CircularOptions<T> {
buffer?: T[];
append?: 'head' | 'tail';
capacity?: number;
}
/**
* Circular vector uses a single buffer to capture a stream of values
* overwriting the oldest value on add.
*
* This supports addting to the 'head' or 'tail' and will grow the buffer
* to match a configured capacity.
*/
export class CircularVector<T = any> implements MutableVector<T> {
private buffer: T[];
private index: number;
private capacity: number;
private tail: boolean;
constructor(options: CircularOptions<T>) {
this.buffer = options.buffer || [];
this.capacity = this.buffer.length;
this.tail = 'head' !== options.append;
this.index = 0;
this.add = this.getAddFunction();
if (options.capacity) {
this.setCapacity(options.capacity);
}
}
/**
* This gets the appropriate add function depending on the buffer state:
* * head vs tail
* * growing buffer vs overwriting values
*/
private getAddFunction() {
// When we are not at capacity, it should actually modify the buffer
if (this.capacity > this.buffer.length) {
if (this.tail) {
return (value: T) => {
this.buffer.push(value);
if (this.buffer.length >= this.capacity) {
this.add = this.getAddFunction();
}
};
} else {
return (value: T) => {
this.buffer.unshift(value);
if (this.buffer.length >= this.capacity) {
this.add = this.getAddFunction();
}
};
}
}
if (this.tail) {
return (value: T) => {
this.buffer[this.index] = value;
this.index = (this.index + 1) % this.buffer.length;
};
}
// Append values to the head
return (value: T) => {
let idx = this.index - 1;
if (idx < 0) {
idx = this.buffer.length - 1;
}
this.buffer[idx] = value;
this.index = idx;
};
}
setCapacity(v: number) {
if (this.capacity === v) {
return;
}
// Make a copy so it is in order and new additions can be at the head or tail
const copy = this.toArray();
if (v > this.length) {
this.buffer = copy;
} else if (v < this.capacity) {
// Shrink the buffer
const delta = this.length - v;
if (this.tail) {
this.buffer = copy.slice(delta, copy.length); // Keep last items
} else {
this.buffer = copy.slice(0, copy.length - delta); // Keep first items
}
}
this.capacity = v;
this.index = 0;
this.add = this.getAddFunction();
}
setAppendMode(mode: 'head' | 'tail') {
const tail = 'head' !== mode;
if (tail !== this.tail) {
this.buffer = this.toArray().reverse();
this.index = 0;
this.tail = tail;
this.add = this.getAddFunction();
}
}
reverse() {
this.buffer.reverse();
}
/**
* Add the value to the buffer
*/
add: (value: T) => void;
get(index: number) {
return this.buffer[(index + this.index) % this.buffer.length];
}
set(index: number, value: T) {
this.buffer[(index + this.index) % this.buffer.length] = value;
}
get length() {
return this.buffer.length;
}
toArray(): T[] {
return vectorToArray(this);
}
toJSON(): T[] {
return vectorToArray(this);
}
}
interface AppendedVectorInfo<T> {
start: number;
end: number;
values: Vector<T>;
}
/**
* This may be more trouble than it is worth. This trades some computation time for
* RAM -- rather than allocate a new array the size of all previous arrays, this just
* points the correct index to their original array values
*/
export class AppendedVectors<T = any> implements Vector<T> {
length = 0;
source: Array<AppendedVectorInfo<T>> = new Array<AppendedVectorInfo<T>>();
constructor(startAt = 0) {
this.length = startAt;
}
/**
* Make the vector look like it is this long
*/
setLength(length: number) {
if (length > this.length) {
// make the vector longer (filling with undefined)
this.length = length;
} else if (length < this.length) {
// make the array shorter
const sources: Array<AppendedVectorInfo<T>> = new Array<AppendedVectorInfo<T>>();
for (const src of this.source) {
sources.push(src);
if (src.end > length) {
src.end = length;
break;
}
}
this.source = sources;
this.length = length;
}
}
append(v: Vector<T>): AppendedVectorInfo<T> {
const info = {
start: this.length,
end: this.length + v.length,
values: v,
};
this.length = info.end;
this.source.push(info);
return info;
}
get(index: number): T {
for (let i = 0; i < this.source.length; i++) {
const src = this.source[i];
if (index >= src.start && index < src.end) {
return src.values.get(index - src.start);
}
}
return (undefined as unknown) as T;
}
toArray(): T[] {
return vectorToArray(this);
}
toJSON(): T[] {
return vectorToArray(this);
}
}
@@ -0,0 +1,23 @@
import { ArrayVector } from './ArrayVector';
import { AppendedVectors } from './AppendedVectors';
describe('Check Appending Vector', () => {
it('should transparently join them', () => {
const appended = new AppendedVectors();
appended.append(new ArrayVector([1, 2, 3]));
appended.append(new ArrayVector([4, 5, 6]));
appended.append(new ArrayVector([7, 8, 9]));
expect(appended.length).toEqual(9);
appended.setLength(5);
expect(appended.length).toEqual(5);
appended.append(new ArrayVector(['a', 'b', 'c']));
expect(appended.length).toEqual(8);
expect(appended.toArray()).toEqual([1, 2, 3, 4, 5, 'a', 'b', 'c']);
appended.setLength(2);
appended.setLength(6);
appended.append(new ArrayVector(['x', 'y', 'z']));
expect(appended.toArray()).toEqual([1, 2, undefined, undefined, undefined, undefined, 'x', 'y', 'z']);
});
});
@@ -0,0 +1,73 @@
import { Vector } from '../types/vector';
import { vectorToArray } from './vectorToArray';
interface AppendedVectorInfo<T> {
start: number;
end: number;
values: Vector<T>;
}
/**
* This may be more trouble than it is worth. This trades some computation time for
* RAM -- rather than allocate a new array the size of all previous arrays, this just
* points the correct index to their original array values
*/
export class AppendedVectors<T = any> implements Vector<T> {
length = 0;
source: Array<AppendedVectorInfo<T>> = new Array<AppendedVectorInfo<T>>();
constructor(startAt = 0) {
this.length = startAt;
}
/**
* Make the vector look like it is this long
*/
setLength(length: number) {
if (length > this.length) {
// make the vector longer (filling with undefined)
this.length = length;
} else if (length < this.length) {
// make the array shorter
const sources: Array<AppendedVectorInfo<T>> = new Array<AppendedVectorInfo<T>>();
for (const src of this.source) {
sources.push(src);
if (src.end > length) {
src.end = length;
break;
}
}
this.source = sources;
this.length = length;
}
}
append(v: Vector<T>): AppendedVectorInfo<T> {
const info = {
start: this.length,
end: this.length + v.length,
values: v,
};
this.length = info.end;
this.source.push(info);
return info;
}
get(index: number): T {
for (let i = 0; i < this.source.length; i++) {
const src = this.source[i];
if (index >= src.start && index < src.end) {
return src.values.get(index - src.start);
}
}
return (undefined as unknown) as T;
}
toArray(): T[] {
return vectorToArray(this);
}
toJSON(): T[] {
return vectorToArray(this);
}
}
@@ -0,0 +1,37 @@
import { MutableVector } from '../types/vector';
export class ArrayVector<T = any> implements MutableVector<T> {
buffer: T[];
constructor(buffer?: T[]) {
this.buffer = buffer ? buffer : [];
}
get length() {
return this.buffer.length;
}
add(value: T) {
this.buffer.push(value);
}
get(index: number): T {
return this.buffer[index];
}
set(index: number, value: T) {
this.buffer[index] = value;
}
reverse() {
this.buffer.reverse();
}
toArray(): T[] {
return this.buffer;
}
toJSON(): T[] {
return this.buffer;
}
}
@@ -1,31 +1,4 @@
import { ConstantVector, ScaledVector, ArrayVector, CircularVector, AppendedVectors } from './vector';
describe('Check Proxy Vector', () => {
it('should support constant values', () => {
const value = 3.5;
const v = new ConstantVector(value, 7);
expect(v.length).toEqual(7);
expect(v.get(0)).toEqual(value);
expect(v.get(1)).toEqual(value);
// Now check all of them
for (let i = 0; i < 10; i++) {
expect(v.get(i)).toEqual(value);
}
});
it('should support multiply operations', () => {
const source = new ArrayVector([1, 2, 3, 4]);
const scale = 2.456;
const v = new ScaledVector(source, scale);
expect(v.length).toEqual(source.length);
// expect(v.push(10)).toEqual(source.length); // not implemented
for (let i = 0; i < 10; i++) {
expect(v.get(i)).toEqual(source.get(i) * scale);
}
});
});
import { CircularVector } from './CircularVector';
describe('Check Circular Vector', () => {
it('should append values', () => {
@@ -156,24 +129,3 @@ describe('Check Circular Vector', () => {
expect(v.toArray()).toEqual([3, 4, 5]);
});
});
describe('Check Appending Vector', () => {
it('should transparently join them', () => {
const appended = new AppendedVectors();
appended.append(new ArrayVector([1, 2, 3]));
appended.append(new ArrayVector([4, 5, 6]));
appended.append(new ArrayVector([7, 8, 9]));
expect(appended.length).toEqual(9);
appended.setLength(5);
expect(appended.length).toEqual(5);
appended.append(new ArrayVector(['a', 'b', 'c']));
expect(appended.length).toEqual(8);
expect(appended.toArray()).toEqual([1, 2, 3, 4, 5, 'a', 'b', 'c']);
appended.setLength(2);
appended.setLength(6);
appended.append(new ArrayVector(['x', 'y', 'z']));
expect(appended.toArray()).toEqual([1, 2, undefined, undefined, undefined, undefined, 'x', 'y', 'z']);
});
});
@@ -0,0 +1,138 @@
import { MutableVector } from '../types/vector';
import { vectorToArray } from './vectorToArray';
interface CircularOptions<T> {
buffer?: T[];
append?: 'head' | 'tail';
capacity?: number;
}
/**
* Circular vector uses a single buffer to capture a stream of values
* overwriting the oldest value on add.
*
* This supports addting to the 'head' or 'tail' and will grow the buffer
* to match a configured capacity.
*/
export class CircularVector<T = any> implements MutableVector<T> {
private buffer: T[];
private index: number;
private capacity: number;
private tail: boolean;
constructor(options: CircularOptions<T>) {
this.buffer = options.buffer || [];
this.capacity = this.buffer.length;
this.tail = 'head' !== options.append;
this.index = 0;
this.add = this.getAddFunction();
if (options.capacity) {
this.setCapacity(options.capacity);
}
}
/**
* This gets the appropriate add function depending on the buffer state:
* * head vs tail
* * growing buffer vs overwriting values
*/
private getAddFunction() {
// When we are not at capacity, it should actually modify the buffer
if (this.capacity > this.buffer.length) {
if (this.tail) {
return (value: T) => {
this.buffer.push(value);
if (this.buffer.length >= this.capacity) {
this.add = this.getAddFunction();
}
};
} else {
return (value: T) => {
this.buffer.unshift(value);
if (this.buffer.length >= this.capacity) {
this.add = this.getAddFunction();
}
};
}
}
if (this.tail) {
return (value: T) => {
this.buffer[this.index] = value;
this.index = (this.index + 1) % this.buffer.length;
};
}
// Append values to the head
return (value: T) => {
let idx = this.index - 1;
if (idx < 0) {
idx = this.buffer.length - 1;
}
this.buffer[idx] = value;
this.index = idx;
};
}
setCapacity(v: number) {
if (this.capacity === v) {
return;
}
// Make a copy so it is in order and new additions can be at the head or tail
const copy = this.toArray();
if (v > this.length) {
this.buffer = copy;
} else if (v < this.capacity) {
// Shrink the buffer
const delta = this.length - v;
if (this.tail) {
this.buffer = copy.slice(delta, copy.length); // Keep last items
} else {
this.buffer = copy.slice(0, copy.length - delta); // Keep first items
}
}
this.capacity = v;
this.index = 0;
this.add = this.getAddFunction();
}
setAppendMode(mode: 'head' | 'tail') {
const tail = 'head' !== mode;
if (tail !== this.tail) {
this.buffer = this.toArray().reverse();
this.index = 0;
this.tail = tail;
this.add = this.getAddFunction();
}
}
reverse() {
this.buffer.reverse();
}
/**
* Add the value to the buffer
*/
add: (value: T) => void;
get(index: number) {
return this.buffer[(index + this.index) % this.buffer.length];
}
set(index: number, value: T) {
this.buffer[(index + this.index) % this.buffer.length] = value;
}
get length() {
return this.buffer.length;
}
toArray(): T[] {
return vectorToArray(this);
}
toJSON(): T[] {
return vectorToArray(this);
}
}
@@ -0,0 +1,17 @@
import { ConstantVector } from './ConstantVector';
describe('ConstantVector', () => {
it('should support constant values', () => {
const value = 3.5;
const v = new ConstantVector(value, 7);
expect(v.length).toEqual(7);
expect(v.get(0)).toEqual(value);
expect(v.get(1)).toEqual(value);
// Now check all of them
for (let i = 0; i < 10; i++) {
expect(v.get(i)).toEqual(value);
}
});
});
@@ -0,0 +1,22 @@
import { Vector } from '../types/vector';
export class ConstantVector<T = any> implements Vector<T> {
constructor(private value: T, private len: number) {}
get length() {
return this.len;
}
get(index: number): T {
return this.value;
}
toArray(): T[] {
const arr = new Array<T>(this.length);
return arr.fill(this.value);
}
toJSON(): T[] {
return this.toArray();
}
}
@@ -0,0 +1,15 @@
import { ArrayVector } from './ArrayVector';
import { ScaledVector } from './ScaledVector';
describe('ScaledVector', () => {
it('should support multiply operations', () => {
const source = new ArrayVector([1, 2, 3, 4]);
const scale = 2.456;
const v = new ScaledVector(source, scale);
expect(v.length).toEqual(source.length);
// expect(v.push(10)).toEqual(source.length); // not implemented
for (let i = 0; i < 10; i++) {
expect(v.get(i)).toEqual(source.get(i) * scale);
}
});
});
@@ -0,0 +1,22 @@
import { Vector } from '../types/vector';
import { vectorToArray } from './vectorToArray';
export class ScaledVector implements Vector<number> {
constructor(private source: Vector<number>, private scale: number) {}
get length(): number {
return this.source.length;
}
get(index: number): number {
return this.source.get(index) * this.scale;
}
toArray(): number[] {
return vectorToArray(this);
}
toJSON(): number[] {
return vectorToArray(this);
}
}
@@ -0,0 +1,25 @@
import { Vector } from '../types/vector';
import { vectorToArray } from './vectorToArray';
/**
* Values are returned in the order defined by the input parameter
*/
export class SortedVector<T = any> implements Vector<T> {
constructor(private source: Vector<T>, private order: number[]) {}
get length(): number {
return this.source.length;
}
get(index: number): T {
return this.source.get(this.order[index]);
}
toArray(): T[] {
return vectorToArray(this);
}
toJSON(): T[] {
return vectorToArray(this);
}
}
@@ -0,0 +1,6 @@
export * from './AppendedVectors';
export * from './ArrayVector';
export * from './CircularVector';
export * from './ConstantVector';
export * from './ScaledVector';
export * from './SortedVector';
@@ -0,0 +1,9 @@
import { Vector } from '../types/vector';
export function vectorToArray<T>(v: Vector<T>): T[] {
const arr: T[] = [];
for (let i = 0; i < v.length; i++) {
arr[i] = v.get(i);
}
return arr;
}