Organise data frame and vectors code
This commit is contained in:
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-85
@@ -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);
|
||||
});
|
||||
});
|
||||
+7
-126
@@ -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';
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './utils/index';
|
||||
export * from './types/index';
|
||||
export * from './vector';
|
||||
export * from './dataframe';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
-49
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user