475 строки
17 KiB
TypeScript
475 строки
17 KiB
TypeScript
///<reference path='refs.ts'/>
|
|
module TDev.RT {
|
|
//? A collection of objects
|
|
//@ stem("coll") icon("fa-list-ol")
|
|
//@ enumerable serializable ctx(general,json)
|
|
export class Collection<T>
|
|
extends RTValue
|
|
{
|
|
constructor(public typeInfo:any) {
|
|
super();
|
|
}
|
|
|
|
static mkAny(typeInfo:any, a:any[] = []):Collection<any>
|
|
{
|
|
return Collection.mk<any>(typeInfo, a)
|
|
}
|
|
|
|
static mk<T>(a:T[], typeInfo:any):Collection<T>
|
|
{
|
|
return Collection.fromArray(a, typeInfo)
|
|
}
|
|
|
|
static mkStrings(a:string[]):Collection<string>
|
|
{
|
|
return Collection.fromArray(a, "string")
|
|
}
|
|
|
|
static mkNumbers(a:number[]):Collection<number>
|
|
{
|
|
return Collection.fromArray(a, "number")
|
|
}
|
|
|
|
static fromArray<T>(a:T[], typeInfo:any):Collection<T>
|
|
{
|
|
if (!a) return undefined;
|
|
|
|
var r = new Collection<T>(typeInfo);
|
|
r.a = a;
|
|
return r;
|
|
}
|
|
|
|
public a:T[] = [];
|
|
private _continuation:string;
|
|
|
|
public get_enumerator() { return this.a.slice(0); }
|
|
|
|
//? Gets the number of objects.
|
|
public count() : number { return this.a.length; }
|
|
|
|
//? Removes all objects from the collection
|
|
//@ writesMutable
|
|
public clear() : void { this.a = []; }
|
|
|
|
//? Adds an object
|
|
//@ writesMutable
|
|
public add(item:T) : void { this.a.push(item); }
|
|
|
|
//? Adds many objects at once
|
|
//@ writesMutable
|
|
public add_many(items:Collection<T>) : void { this.a.pushRange(items.a.slice(0)); }
|
|
|
|
//? Gets the index of the first occurrence of an object. Returns -1 if not found or start is out of range.
|
|
public index_of(item:T, start:number) : number
|
|
{
|
|
if (Util.isOOB(start, this.count())) return -1;
|
|
for (var i = Util.indexCheck(start, this.count()); i < this.a.length; ++i)
|
|
if (this.a[i] === item) return i;
|
|
return -1;
|
|
}
|
|
|
|
//? Gets the object at position index. Returns invalid if index is out of range
|
|
public at(index:number) : T { return this.a[Math.floor(index)]; }
|
|
|
|
//? Checks if the item is in the collection
|
|
public contains(item:T) : boolean
|
|
{
|
|
var i = this.index_of(item, 0);
|
|
return (i >= 0);
|
|
}
|
|
|
|
//? Removes the first occurence of an object. Returns true if removed.
|
|
//@ writesMutable ignoreReturnValue
|
|
public remove(item:T) : boolean
|
|
{
|
|
var i = this.index_of(item, 0);
|
|
if (i >= 0) {
|
|
this.a.splice(i, 1);
|
|
return true;
|
|
} else return false;
|
|
}
|
|
|
|
//? Removes the object at position index.
|
|
//@ writesMutable
|
|
public remove_at(index:number) : void
|
|
{
|
|
if (Util.isOOB(index, this.count())) return;
|
|
this.a.splice(Util.indexCheck(index, this.count()), 1);
|
|
}
|
|
|
|
//? Reverses the order of objects in the collection
|
|
//@ writesMutable
|
|
public reverse() : void { this.a.reverse(); }
|
|
|
|
//? Gets a random object from the collection. Returns invalid if the collection is empty.
|
|
public random() : T { return this.a.length == 0 ? undefined : this.at(Math_.random(this.a.length)); }
|
|
|
|
//? Sets the object at position index. Does nothing if the index is out of range.
|
|
//@ writesMutable
|
|
public set_at(index:number, item:T) : void
|
|
{
|
|
var _index = Math.floor(index);
|
|
if (0 <= _index && index < this.a.length)
|
|
this.a[_index] = item;
|
|
}
|
|
|
|
//? Inserts an object at position index. Does nothing if index is out of range.
|
|
//@ writesMutable
|
|
public insert_at(index:number, item:T) : void
|
|
{
|
|
if (Util.isOOB(index, this.count() + 1)) return;
|
|
this.a.splice(Util.indexCheck(index, this.count() + 1), 0, item);
|
|
}
|
|
|
|
//? Exports a JSON representation of the contents.
|
|
//@ readsMutable [result].writesMutable
|
|
public to_json(sf: IStackFrame): JsonObject {
|
|
var ctx = new JsonExportCtx(sf);
|
|
ctx.push(this);
|
|
var json = this.exportJson(ctx);
|
|
ctx.pop(this);
|
|
return JsonObject.wrap(json);
|
|
}
|
|
|
|
//? Imports a JSON representation of the contents.
|
|
//@ writesMutable
|
|
public from_json(jobj: JsonObject, sf:IStackFrame): void {
|
|
this.importJson(new JsonImportCtx(sf), jobj.value());
|
|
}
|
|
|
|
//? Returns a collections of elements that satisfy the filter `condition`
|
|
//@ readsMutable [result].writesMutable
|
|
public where(condition:Predicate<T>, s:IStackFrame) : Collection<T>
|
|
{
|
|
var rt = s.rt
|
|
return Collection.fromArray(this.a.filter(e => !!rt.runUserAction(condition, [e])), this.typeInfo)
|
|
}
|
|
|
|
//? Applies `converter` on all elements of the input collection and returns a collection of results
|
|
//@ readsMutable [result].writesMutable
|
|
public map_to<S>(converter:Converter<T,S>, s:IStackFrame, type_S:any) : Collection<S>
|
|
{
|
|
var rt = s.rt
|
|
return Collection.fromArray(this.a.map(e => <S>rt.runUserAction(converter, [e])), type_S)
|
|
}
|
|
|
|
//? Returns a collection sorted using specified `comparison` function
|
|
//@ readsMutable [result].writesMutable
|
|
public sorted(comparison:Comparison<T>, s:IStackFrame) : Collection<T>
|
|
{
|
|
var rt = s.rt
|
|
return Collection.fromArray(this.a.stableSorted((a, b) => rt.runValidUserAction(comparison, [a, b])), this.typeInfo)
|
|
}
|
|
|
|
//? Returns a collection sorted using specified comparison key
|
|
//@ readsMutable [result].writesMutable
|
|
public ordered_by(key:NumberConverter<T>, s:IStackFrame) : Collection<T>
|
|
{
|
|
var rt = s.rt
|
|
return Collection.fromArray(this.a.stableSorted((a, b) => {
|
|
return rt.runValidUserAction(key, [a]) - rt.runValidUserAction(key, [b])
|
|
}), this.typeInfo)
|
|
}
|
|
|
|
//? Returns a collection sorted using specified comparison key
|
|
//@ readsMutable [result].writesMutable
|
|
public ordered_by_string(key:StringConverter<T>, s:IStackFrame) : Collection<T>
|
|
{
|
|
var rt = s.rt
|
|
return Collection.fromArray(this.a.stableSorted((a, b) => {
|
|
return rt.runValidUserAction(key, [a]).localeCompare(rt.runValidUserAction(key, [b]))
|
|
}), this.typeInfo)
|
|
}
|
|
|
|
//? Returns a collection with the `count` first elements if any.
|
|
//@ readsMutable [result].writesMutable
|
|
public take(count: number, s: IStackFrame): Collection<T> {
|
|
return Collection.fromArray(this.a.slice(0, Math.floor(count)), this.typeInfo);
|
|
}
|
|
|
|
//? Returns a slice of the collection starting at `start`, and ends at, but does not include, the `end`.
|
|
//@ readsMutable [result].writesMutable
|
|
public slice(start: number, end: number): Collection<T> {
|
|
return Collection.fromArray(this.a.slice(Math.floor(start), Math.floor(end)), this.typeInfo);
|
|
}
|
|
|
|
//? Gets the first element if any
|
|
public first(): T {
|
|
return this.at(0);
|
|
}
|
|
|
|
//? Gets the last element if any
|
|
public last(): T {
|
|
return this.at(this.count() - 1);
|
|
}
|
|
|
|
public jsonExportKey(ctx: JsonExportCtx) {
|
|
return null; // conservative - recursion is possible if wrapped type is recursive
|
|
}
|
|
|
|
public exportJson(ctx: JsonExportCtx): any {
|
|
|
|
if (this.a.length > 0 && (this.a[0]["exportJson"] === undefined)) {
|
|
var a0 = this.a[0]
|
|
if (typeof a0 == "string" || typeof a0 == "number" || typeof a0 == "boolean") {
|
|
// primitives are OK
|
|
} else
|
|
Util.userError(lf("json export is not supported for this type"));
|
|
}
|
|
|
|
return ctx.encodeArrayNode(this, this.a.slice(0));
|
|
}
|
|
|
|
public importJson(ctx: JsonImportCtx, json: any): RT.RTValue
|
|
{
|
|
var prev = this.a;
|
|
this.a = []
|
|
|
|
if (!Array.isArray(json)) return this
|
|
|
|
if (typeof this.typeInfo == "string") {
|
|
if (this.typeInfo === "number")
|
|
json.forEach(n => { if (typeof n === "number") this.a.push(<any>n) })
|
|
else if (this.typeInfo === "string")
|
|
json.forEach(n => { if (typeof n === "string") this.a.push(<any>n) })
|
|
else if (this.typeInfo === "boolean")
|
|
json.forEach(n => { if (typeof n === "boolean") this.a.push(<any>n) })
|
|
else
|
|
Util.userError("json import is not supported for Collection of " + this.typeInfo);
|
|
} else if (this.typeInfo instanceof RecordSingleton) {
|
|
for (var n = 0; n < json.length; n++) {
|
|
var o = ctx.importRecord(json, prev[n], n, this.typeInfo);
|
|
if (o)
|
|
this.a.push(<any>o);
|
|
}
|
|
} else if (typeof this.typeInfo === "function") {
|
|
var ctor = this.typeInfo
|
|
json.forEach(n => {
|
|
var v = new ctor()
|
|
v = v.importJson(ctx, n)
|
|
if (v) this.a.push(v)
|
|
})
|
|
} else {
|
|
Util.userError("json import is not supported for this Collection")
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
private getHtml()
|
|
{
|
|
var first = undefined
|
|
for (var i = 0; i < this.a.length; ++i)
|
|
if (this.a[i]) {
|
|
first = this.a[i];
|
|
break;
|
|
}
|
|
|
|
var s = '[';
|
|
for (var i = 0; i < this.a.length; ++i) {
|
|
if (i > 0) s += ', ';
|
|
s += this.a[i];
|
|
}
|
|
s += ']';
|
|
return span(null, s);
|
|
}
|
|
|
|
private getRecord():RecordSingleton
|
|
{
|
|
if (this.typeInfo instanceof RecordSingleton)
|
|
return <RecordSingleton>this.typeInfo
|
|
return null
|
|
}
|
|
|
|
//? Display all objects on the wall
|
|
public post_to_wall(s : IStackFrame): void
|
|
{
|
|
if (this.getRecord())
|
|
s.rt.postBoxedHtml(this.getRecord().getTable(<any[]>this.a, s), s.pc);
|
|
else if (this.typeInfo.prototype instanceof RTValue)
|
|
(<any[]>this.a).forEach((v: RTValue) => v.post_to_wall(s));
|
|
else
|
|
s.rt.postBoxedHtml(this.getHtml(), s.pc);
|
|
}
|
|
|
|
public debuggerDisplay(clickHandler: () => any): HTMLElement
|
|
{
|
|
var e: HTMLElement;
|
|
if (this.getRecord()) {
|
|
try {
|
|
e = this.getRecord().getTable(<any[]>this.a, null);
|
|
} catch (e) {
|
|
e = span(null, e.message || ""); // can be a "user error" when record originated from stale session
|
|
}
|
|
} else {
|
|
e = this.getHtml()
|
|
}
|
|
|
|
return e.withClick(clickHandler);
|
|
}
|
|
|
|
//? Ask user to pick an entry from this collection
|
|
//@ uiAsync returns(T)
|
|
public pick_entry(text: string, r: ResumeCtx) {
|
|
var rt = r.rt;
|
|
var getView = (o: any) => {
|
|
if (o.getIndexCard) return o.getIndexCard(r.stackframe)
|
|
else if (o.getViewCore) return o.getViewCore(r.stackframe, null)
|
|
else if (o.toString) return o.toString()
|
|
else return o + ""
|
|
};
|
|
var m = new ModalDialog();
|
|
var chosen = null;
|
|
var btns = this.a.map((o: any) => div('modalDialogChooseItem', getView(o)).withClick(() => {
|
|
chosen = o;
|
|
m.dismiss();
|
|
}));
|
|
m.add([div("wall-dialog-header", text)/* ,div("wall-dialog-body", caption)*/]);
|
|
m.onDismiss = () => r.resumeVal(chosen);
|
|
m.choose(btns);
|
|
}
|
|
|
|
//? Computes the sum of the key of the elements in the collection
|
|
//@ readsMutable
|
|
public sum_of(key: NumberConverter<T>, s: IStackFrame): number {
|
|
var rt = s.rt
|
|
var v = this.a.map(x => <number>rt.runValidUserAction(key, [x]));
|
|
return Util.stableSum(v);
|
|
}
|
|
|
|
//? Computes the maximum of the key of the elements in the collection
|
|
//@ readsMutable
|
|
public max_of(key: NumberConverter<T>, s: IStackFrame): number {
|
|
var rt = s.rt
|
|
if (this.a.length == 0) return undefined;
|
|
var m = <number>rt.runValidUserAction(key, [this.a[0]]);
|
|
for (var i = 1; i < this.a.length; ++i) {
|
|
var v = <number>rt.runValidUserAction(key, [this.a[i]]);
|
|
if (v > m) m = v;
|
|
}
|
|
return m;
|
|
}
|
|
|
|
//? Computes the average of the key of the elements in the collection
|
|
//@ readsMutable
|
|
public avg_of(key: NumberConverter<T>, s: IStackFrame): number {
|
|
var rt = s.rt
|
|
if (this.a.length == 0) return undefined;
|
|
return this.sum_of(key, s) / this.count();
|
|
}
|
|
|
|
//? Computes the minimum of the key of the elements in the collection
|
|
//@ readsMutable
|
|
public min_of(key: NumberConverter<T>, s: IStackFrame): number {
|
|
var rt = s.rt
|
|
if (this.a.length == 0) return undefined;
|
|
var m = <number>rt.runValidUserAction(key, [this.a[0]]);
|
|
for (var i = 1; i < this.a.length; ++i) {
|
|
var v = <number>rt.runValidUserAction(key, [this.a[i]]);
|
|
if (v < m) m = v;
|
|
}
|
|
return m;
|
|
}
|
|
|
|
//
|
|
// Methods specific to Collection<some-specific-type>
|
|
//
|
|
|
|
//? Computes the minimum of the values
|
|
//@ readsMutable onlyOn(Number)
|
|
public min(): number
|
|
{
|
|
var a = <number[]><any>this.a
|
|
if (a.length == 0) return undefined;
|
|
return this.a.min();
|
|
}
|
|
|
|
//? Computes the maximum of the values
|
|
//@ readsMutable onlyOn(Number)
|
|
public max() : number
|
|
{
|
|
if (this.a.length == 0) return undefined;
|
|
var a = <number[]><any>this.a
|
|
return a.max();
|
|
}
|
|
|
|
//? Computes the sum of the values
|
|
//@ readsMutable onlyOn(Number)
|
|
public sum(): number {
|
|
if (this.a.length == 0) return 0;
|
|
var a = <number[]><any>this.a
|
|
return Util.stableSum(a);
|
|
}
|
|
|
|
//? Computes the average of the values
|
|
//@ readsMutable onlyOn(Number)
|
|
public avg(): number
|
|
{
|
|
if (this.a.length == 0) return 0;
|
|
return this.sum() / this.a.length;
|
|
}
|
|
|
|
//? Sorts from the newest to oldest
|
|
//@ writesMutable onlyOn(Message)
|
|
public sort_by_date(): void
|
|
{
|
|
(<Message[]><any>this.a).sort(function (m1, m2) {
|
|
if (!m1.time()) return 1;
|
|
if (!m2.time()) return -1;
|
|
return m1.time().d.valueOf() < m2.time().d.valueOf() ? -1 : 1;
|
|
});
|
|
}
|
|
|
|
//? Sorts the strings in this collection
|
|
//@ writesMutable onlyOn(Number, String)
|
|
public sort() : void
|
|
{
|
|
this.a.sort(); // TODO locale?
|
|
}
|
|
|
|
//? Sorts the places by distance to the location
|
|
//@ writesMutable onlyOn(Location, Place)
|
|
public sort_by_distance(loc:Location_) : void
|
|
{
|
|
var getLoc = (l) => {
|
|
if (l instanceof Place) return (<Place>l).location()
|
|
return <Location_>l
|
|
}
|
|
(<any[]>this.a).sort((l, r) => {
|
|
var lloc = getLoc(l)
|
|
var rloc = getLoc(r)
|
|
if (!lloc && !rloc) return 0;
|
|
if (!lloc) return 1;
|
|
if (!rloc) return -1;
|
|
|
|
return rloc.distance(lloc) - lloc.distance(rloc);
|
|
});
|
|
}
|
|
|
|
//? Concatenates the separator and items into a string
|
|
//@ readsMutable onlyOn(Number, String)
|
|
public join(separator:string) : string
|
|
{
|
|
return (<string[]><any>this.a).join(separator);
|
|
}
|
|
|
|
//? Gets the identifier of the next set of items (if any)
|
|
//@ readsMutable
|
|
public continuation(): string { return this._continuation || ""; }
|
|
|
|
//? Sets the identifier of the next set of items
|
|
//@ writesMutable
|
|
public set_continuation(value : string) : void { this._continuation = value; }
|
|
}
|
|
|
|
// Backward-compat for javascript* APIs
|
|
export module StringCollection {
|
|
export function mk(v:string[]) { return Collection.mkStrings(v) }
|
|
export function fromArray(v:string[]) { return Collection.mkStrings(v) }
|
|
}
|
|
export module NumberCollection {
|
|
export function mk(v:number[]) { return Collection.mkNumbers(v) }
|
|
export function fromArray(v:number[]) { return Collection.mkNumbers(v) }
|
|
}
|
|
}
|