mirror of https://github.com/Squidex/squidex.git
74 changed files with 374 additions and 822 deletions
@ -0,0 +1,73 @@ |
|||
|
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
// tslint:disable: readonly-array
|
|||
|
|||
interface ReadonlyArray<T> { |
|||
replaceBy(field: string, value: T): ReadonlyArray<T>; |
|||
|
|||
removeBy(field: string, value: T): ReadonlyArray<T>; |
|||
|
|||
removed(value?: T): ReadonlyArray<T>; |
|||
|
|||
sorted(): ReadonlyArray<T>; |
|||
|
|||
sortedByString(selector: (value: T) => string): ReadonlyArray<T>; |
|||
} |
|||
|
|||
interface Array<T> { |
|||
replaceBy(field: string, value: T): Array<T>; |
|||
|
|||
removeBy(field: string, value: T): Array<T>; |
|||
|
|||
removed(value?: T): Array<T>; |
|||
|
|||
sorted(): Array<T>; |
|||
|
|||
sortedByString(selector: (value: T) => string): Array<T>; |
|||
} |
|||
|
|||
Array.prototype.replaceBy = function<T>(field: string, value: T) { |
|||
if (!value) { |
|||
return this; |
|||
} |
|||
|
|||
return this.map((v: T) => v[field] === value[field] ? value : v); |
|||
}; |
|||
|
|||
Array.prototype.removeBy = function<T>(field: string, value: T) { |
|||
if (!value) { |
|||
return this; |
|||
} |
|||
|
|||
return this.filter((v: T) => v[field] !== value[field]); |
|||
}; |
|||
|
|||
Array.prototype.removed = function<T>(value?: T) { |
|||
if (!value) { |
|||
return this; |
|||
} |
|||
|
|||
return this.filter((v: T) => v !== value); |
|||
}; |
|||
|
|||
Array.prototype.sorted = function<T>() { |
|||
const copy = [...this]; |
|||
|
|||
copy.sort(); |
|||
|
|||
return copy; |
|||
}; |
|||
|
|||
Array.prototype.sortedByString = function<T>(selector: (value: T) => string) { |
|||
const copy = [...this]; |
|||
|
|||
copy.sort((a, b) => selector(a).localeCompare(selector(b), undefined, { sensitivity: 'base' })); |
|||
|
|||
return copy; |
|||
}; |
|||
@ -0,0 +1,10 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
export function compareStrings(a: string, b: string) { |
|||
return a.localeCompare(b, undefined, { sensitivity: 'base' }); |
|||
} |
|||
@ -1,215 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { ImmutableArray } from './immutable-array'; |
|||
|
|||
describe('ImmutableArray', () => { |
|||
it('should create empty instance', () => { |
|||
const array_1 = ImmutableArray.of(); |
|||
|
|||
expect(array_1.length).toBe(0); |
|||
}); |
|||
|
|||
it('should create same instance for empty arrays', () => { |
|||
const array_a = ImmutableArray.of(); |
|||
const array_b = ImmutableArray.of(); |
|||
const array_c = ImmutableArray.of([]); |
|||
const array_d = ImmutableArray.empty(); |
|||
|
|||
expect(array_b).toBe(<any>array_a); |
|||
expect(array_c).toBe(<any>array_a); |
|||
expect(array_d).toBe(<any>array_a); |
|||
}); |
|||
|
|||
it('should create non empty instance', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3]); |
|||
|
|||
expect(array_1.length).toBe(3); |
|||
expect(array_1.values).toEqual([1, 2, 3]); |
|||
}); |
|||
|
|||
it('should push items', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3]); |
|||
const array_2 = array_1.push(4, 5); |
|||
|
|||
expect(array_2.length).toBe(5); |
|||
expect(array_2.values).toEqual([1, 2, 3, 4, 5]); |
|||
}); |
|||
|
|||
it('should return same array if pushing zero items', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3]); |
|||
const array_2 = array_1.push(); |
|||
|
|||
expect(array_2).toBe(array_1); |
|||
}); |
|||
|
|||
it('should push front items', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3]); |
|||
const array_2 = array_1.pushFront(4, 5); |
|||
|
|||
expect(array_2.length).toBe(5); |
|||
expect(array_2.values).toEqual([4, 5, 1, 2, 3]); |
|||
}); |
|||
|
|||
it('should return same array if pushing zero items to the front', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3]); |
|||
const array_2 = array_1.pushFront(); |
|||
|
|||
expect(array_2).toBe(array_1); |
|||
}); |
|||
|
|||
it('should remove item', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3]); |
|||
const array_2 = array_1.remove(2); |
|||
|
|||
expect(array_2.length).toBe(2); |
|||
expect(array_2.values).toEqual([1, 3]); |
|||
}); |
|||
|
|||
it('should return same array if removing zero items', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3]); |
|||
const array_2 = array_1.remove(); |
|||
|
|||
expect(array_2).toBe(array_1); |
|||
}); |
|||
|
|||
it('should remove all by predicate', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3]); |
|||
const array_2 = array_1.removeAll((i: number) => i % 2 === 0); |
|||
|
|||
expect(array_2.values).toEqual([1, 3]); |
|||
}); |
|||
|
|||
it('should return original if nothing has been removed', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3]); |
|||
const array_2 = array_1.removeAll((i: number) => i % 200 === 0); |
|||
|
|||
expect(array_2).toEqual(array_1); |
|||
}); |
|||
|
|||
it('should replace item if found', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3]); |
|||
const array_2 = array_1.replace(2, 4); |
|||
|
|||
expect(array_2.values).toEqual([1, 4, 3]); |
|||
}); |
|||
|
|||
it('should not replace item if not found', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3]); |
|||
const array_2 = array_1.replace(5, 5); |
|||
|
|||
expect(array_2).toBe(array_1); |
|||
}); |
|||
|
|||
it('should replace all by predicate', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3, 4]); |
|||
const array_2 = array_1.replaceAll((i: number) => i % 2 === 0, i => i * 2); |
|||
|
|||
expect(array_2.values).toEqual([1, 4, 3, 8]); |
|||
}); |
|||
|
|||
it('should replace by field', () => { |
|||
const array_1 = ImmutableArray.of([{ id: 1, v: 1 }, { id: 2, v: 2 }]); |
|||
const array_2 = array_1.replaceBy('id', { id: 1, v: 11 }); |
|||
|
|||
expect(array_2.values).toEqual([{ id: 1, v: 11 }, { id: 2, v: 2 }]); |
|||
}); |
|||
|
|||
it('should return original if nothing has been replace', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3, 4]); |
|||
const array_2 = array_1.replaceAll((i: number) => i % 200 === 0, i => i); |
|||
|
|||
expect(array_2).toBe(array_1); |
|||
}); |
|||
|
|||
it('should filter items', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3, 4]); |
|||
const array_2 = array_1.filter((i: number) => i % 2 === 0); |
|||
|
|||
expect(array_2.values).toEqual([2, 4]); |
|||
}); |
|||
|
|||
it('should map items', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3, 4]); |
|||
const array_2 = array_1.map((i: number) => i * 2); |
|||
|
|||
expect(array_2.values).toEqual([2, 4, 6, 8]); |
|||
}); |
|||
|
|||
it('should find item', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3, 4]); |
|||
const result = array_1.find(i => i >= 2.5); |
|||
|
|||
expect(result).toEqual(3); |
|||
}); |
|||
|
|||
it('should not return item if not found', () => { |
|||
const array_1 = ImmutableArray.of([1, 2, 3, 4]); |
|||
const result = array_1.find(i => i >= 4.5); |
|||
|
|||
expect(result).toBeUndefined(); |
|||
}); |
|||
|
|||
it('should sort items', () => { |
|||
const array_1 = ImmutableArray.of([3, 1, 4, 2]); |
|||
const array_2 = array_1.sort((x, y) => x - y); |
|||
|
|||
expect(array_2.values).toEqual([1, 2, 3, 4]); |
|||
}); |
|||
|
|||
it('should sort ascending by numbers', () => { |
|||
const array_1 = ImmutableArray.of([{ id: 3 }, { id: 2 }, { id: 1 }]); |
|||
const array_2 = array_1.sortByNumberAsc(x => x.id); |
|||
|
|||
expect(array_2.values).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]); |
|||
}); |
|||
|
|||
it('should sort descending by numbers', () => { |
|||
const array_1 = ImmutableArray.of([{ id: 1 }, { id: 2 }, { id: 3 }]); |
|||
const array_2 = array_1.sortByNumberDesc(x => x.id); |
|||
|
|||
expect(array_2.values).toEqual([{ id: 3 }, { id: 2 }, { id: 1 }]); |
|||
}); |
|||
|
|||
it('should sort ascending by string', () => { |
|||
const array_1 = ImmutableArray.of([{ id: '3' }, { id: '2' }, { id: '1' }]); |
|||
const array_2 = array_1.sortByStringAsc(x => x.id); |
|||
|
|||
expect(array_2.values).toEqual([{ id: '1' }, { id: '2' }, { id: '3' }]); |
|||
}); |
|||
|
|||
it('should sort descending by string', () => { |
|||
const array_1 = ImmutableArray.of([{ id: '1' }, { id: '2' }, { id: '3' }]); |
|||
const array_2 = array_1.sortByStringDesc(x => x.id); |
|||
|
|||
expect(array_2.values).toEqual([{ id: '3' }, { id: '2' }, { id: '1' }]); |
|||
}); |
|||
|
|||
it('should provide mutable values', () => { |
|||
const array_1 = ImmutableArray.of([3, 1, 4, 2]); |
|||
|
|||
expect(array_1.mutableValues).toBe(array_1.mutableValues); |
|||
}); |
|||
|
|||
it('should provider value at index', () => { |
|||
const array_1 = ImmutableArray.of([3, 1, 4, 2]); |
|||
|
|||
expect(array_1.at(2)).toBe(4); |
|||
}); |
|||
|
|||
it('should iterate over array items', () => { |
|||
const array_1 = ImmutableArray.of([3, 1, 4, 2]); |
|||
|
|||
const values: number[] = []; |
|||
|
|||
for (let iter = array_1[Symbol.iterator](), _step: any; !(_step = iter.next()).done; ) { |
|||
values.push(_step.value); |
|||
} |
|||
|
|||
expect(values).toEqual([3, 1, 4, 2]); |
|||
}); |
|||
}); |
|||
@ -1,233 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
export interface IdField { |
|||
id: string; |
|||
} |
|||
|
|||
function freeze<T>(items: ReadonlyArray<T>): ReadonlyArray<T> { |
|||
for (const item of items) { |
|||
Object.freeze(item); |
|||
} |
|||
|
|||
return items; |
|||
} |
|||
|
|||
export class ImmutableArray<T> implements Iterable<T> { |
|||
private static readonly EMPTY = new ImmutableArray<any>([]); |
|||
private readonly items: ReadonlyArray<T>; |
|||
|
|||
public [Symbol.iterator](): Iterator<T> { |
|||
return this.items.values(); |
|||
} |
|||
|
|||
public get length(): number { |
|||
return this.items.length; |
|||
} |
|||
|
|||
public get values(): ReadonlyArray<T> { |
|||
return [...this.items]; |
|||
} |
|||
|
|||
public get mutableValues(): ReadonlyArray<T> { |
|||
return this.items; |
|||
} |
|||
|
|||
private constructor(items: ReadonlyArray<T>) { |
|||
this.items = items; |
|||
} |
|||
|
|||
public static empty<V>(): ImmutableArray<V> { |
|||
return ImmutableArray.EMPTY; |
|||
} |
|||
|
|||
public static of<V>(items?: ReadonlyArray<V>): ImmutableArray<V> { |
|||
if (!items || items.length === 0) { |
|||
return ImmutableArray.EMPTY; |
|||
} else { |
|||
return new ImmutableArray<V>(freeze([...items])); |
|||
} |
|||
} |
|||
|
|||
public at(index: number) { |
|||
return this.items[index]; |
|||
} |
|||
|
|||
public each(action: (item: T, index?: number) => void) { |
|||
for (let i = 0; i < this.items.length; i++) { |
|||
action(this.items[i], i); |
|||
} |
|||
} |
|||
|
|||
public map<R>(projection: (item: T) => R): ImmutableArray<R> { |
|||
return new ImmutableArray<R>(freeze(this.items.map(v => projection(v!)))); |
|||
} |
|||
|
|||
public filter(predicate: (item: T) => boolean): ImmutableArray<T> { |
|||
return new ImmutableArray<T>(this.items.filter(v => predicate(v!))); |
|||
} |
|||
|
|||
public find(predicate: (item: T, index: number) => boolean): T | undefined { |
|||
return this.items.find(predicate); |
|||
} |
|||
|
|||
public slice(start?: number, end?: number) { |
|||
return new ImmutableArray<T>(this.items.slice(start, end)); |
|||
} |
|||
|
|||
public sort(compareFn?: (a: T, b: T) => number): ImmutableArray<T> { |
|||
const clone = [...this.items]; |
|||
|
|||
clone.sort(compareFn); |
|||
|
|||
return new ImmutableArray<T>(clone); |
|||
} |
|||
|
|||
public sortByStringAsc(filter: (a: T) => string): ImmutableArray<T> { |
|||
return this.sort((a, b) => compareStringsAsc(filter(a), filter(b))); |
|||
} |
|||
|
|||
public sortByStringDesc(filter: (a: T) => string): ImmutableArray<T> { |
|||
return this.sort((a, b) => compareStringsDesc(filter(a), filter(b))); |
|||
} |
|||
|
|||
public sortByNumberAsc(filter: (a: T) => number): ImmutableArray<T> { |
|||
return this.sort((a, b) => compareNumbersAsc(filter(a), filter(b))); |
|||
} |
|||
|
|||
public sortByNumberDesc(filter: (a: T) => number): ImmutableArray<T> { |
|||
return this.sort((a, b) => compareNumbersDesc(filter(a), filter(b))); |
|||
} |
|||
|
|||
public pushFront(...items: ReadonlyArray<T>): ImmutableArray<T> { |
|||
if (items.length === 0) { |
|||
return this; |
|||
} |
|||
return new ImmutableArray<T>([...freeze(items), ...this.items]); |
|||
} |
|||
|
|||
public push(...items: ReadonlyArray<T>): ImmutableArray<T> { |
|||
if (items.length === 0) { |
|||
return this; |
|||
} |
|||
return new ImmutableArray<T>([...this.items, ...freeze(items)]); |
|||
} |
|||
|
|||
public remove(...items: ReadonlyArray<T>): ImmutableArray<T> { |
|||
if (items.length === 0) { |
|||
return this; |
|||
} |
|||
|
|||
const copy = this.items.slice(); |
|||
|
|||
for (const item of items) { |
|||
const index = copy.indexOf(item); |
|||
|
|||
if (index >= 0) { |
|||
copy.splice(index, 1); |
|||
} |
|||
} |
|||
|
|||
return new ImmutableArray<T>(copy); |
|||
} |
|||
|
|||
public removeAll(predicate: (item: T, index: number) => boolean): ImmutableArray<T> { |
|||
const copy = this.items.slice(); |
|||
|
|||
let hasChange = false; |
|||
|
|||
for (let i = 0; i < copy.length; ) { |
|||
if (predicate(copy[i], i)) { |
|||
copy.splice(i, 1); |
|||
|
|||
hasChange = true; |
|||
} else { |
|||
++i; |
|||
} |
|||
} |
|||
|
|||
return hasChange ? new ImmutableArray<T>(copy) : this; |
|||
} |
|||
|
|||
public replace(oldItem: T, newItem: T): ImmutableArray<T> { |
|||
const index = this.items.indexOf(oldItem); |
|||
|
|||
if (index >= 0) { |
|||
if (newItem) { |
|||
Object.freeze(newItem); |
|||
} |
|||
|
|||
const copy = [...this.items.slice(0, index), newItem, ...this.items.slice(index + 1)]; |
|||
|
|||
return new ImmutableArray<T>(copy); |
|||
} else { |
|||
return this; |
|||
} |
|||
} |
|||
|
|||
public replaceAll(predicate: (item: T, index: number) => boolean, replacer: (item: T) => T): ImmutableArray<T> { |
|||
const copy = this.items.slice(); |
|||
|
|||
let hasChange = false; |
|||
|
|||
for (let i = 0; i < copy.length; i++) { |
|||
if (predicate(copy[i], i)) { |
|||
const newItem = replacer(copy[i]); |
|||
|
|||
if (newItem) { |
|||
Object.freeze(newItem); |
|||
} |
|||
|
|||
if (copy[i] !== newItem) { |
|||
copy[i] = newItem; |
|||
|
|||
hasChange = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return hasChange ? new ImmutableArray<T>(copy) : this; |
|||
} |
|||
|
|||
public replaceBy(field: string, newValue: T, replacer?: (o: T, n: T) => T) { |
|||
return this.replaceAll(x => x[field] === newValue[field], o => replacer ? replacer(o, newValue) : newValue); |
|||
} |
|||
|
|||
public removeBy(field: string, value: T) { |
|||
return this.removeAll(x => x[field] === value[field]); |
|||
} |
|||
} |
|||
|
|||
export function compareStringsAsc(a: string, b: string) { |
|||
return a.localeCompare(b, undefined, { sensitivity: 'base' }); |
|||
} |
|||
|
|||
export function compareStringsDesc(a: string, b: string) { |
|||
return b.localeCompare(a, undefined, { sensitivity: 'base' }); |
|||
} |
|||
|
|||
export function compareNumbersAsc(a: number, b: number) { |
|||
if (a < b) { |
|||
return -1; |
|||
} |
|||
if (a > b) { |
|||
return 1; |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
export function compareNumbersDesc(a: number, b: number) { |
|||
if (a < b) { |
|||
return 1; |
|||
} |
|||
if (a > b) { |
|||
return -1; |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
Loading…
Reference in new issue