import {throwNullException} from './NullException'; import {StringBuilder} from './StringBuilder'; import {ListDefinition} from './ListDefinition'; import {Story} from './Story'; export class InkListItem implements IInkListItem{ // InkListItem is a struct public readonly originName: string | null = null; public readonly itemName: string | null = null; constructor(originName: string | null, itemName: string | null) constructor(fullName: string | null) constructor(){ if (typeof arguments[1] !== 'undefined'){ let originName = arguments[0] as string | null; let itemName = arguments[1] as string | null; this.originName = originName; this.itemName = itemName; } else if (arguments[0]){ let fullName = arguments[0] as string; let nameParts = fullName.toString().split('.'); this.originName = nameParts[0]; this.itemName = nameParts[1]; } } public static get Null(){ return new InkListItem(null, null); } public get isNull(){ return this.originName == null && this.itemName == null; } get fullName(){ return ((this.originName !== null) ? this.originName : '?') + '.' + this.itemName; } public toString(): string { return this.fullName; } public Equals(obj: InkListItem){ if (obj instanceof InkListItem) { let otherItem = obj; return otherItem.itemName == this.itemName && otherItem.originName == this.originName; } return false; } // These methods did not exist in the original C# code. Their purpose is to // make `InkListItem` mimics the value-type semantics of the original // struct. Please refer to the end of this file, for a more in-depth // explanation. /** * Returns a shallow clone of the current instance. */ public copy(){ return new InkListItem(this.originName, this.itemName); } /** * Returns a `SerializedInkListItem` representing the current * instance. The result is intended to be used as a key inside a Map. */ public serialized(): SerializedInkListItem{ // We are simply using a JSON representation as a value-typed key. return JSON.stringify({originName: this.originName, itemName: this.itemName}); } /** * Reconstructs a `InkListItem` from the given SerializedInkListItem. */ public static fromSerializedKey(key: SerializedInkListItem): InkListItem { let obj = JSON.parse(key); if (!InkListItem.isLikeInkListItem(obj)) return InkListItem.Null; let inkListItem = obj as IInkListItem; return new InkListItem(inkListItem.originName, inkListItem.itemName); } /** * Determines whether the given item is sufficiently `InkListItem`-like * to be used as a template when reconstructing the InkListItem. */ private static isLikeInkListItem(item: any){ if (typeof item !== 'object') return false; if (!item.hasOwnProperty('originName') || !item.hasOwnProperty('itemName')) return false; if (typeof item.originName !== 'string' && typeof item.originName !== null) return false; if (typeof item.itemName !== 'string' && typeof item.itemName !== null) return false; return true; } } export class InkList extends Map { public origins: ListDefinition[] | null = null; public _originNames: string[] | null = []; constructor() constructor(otherList: InkList) constructor(singleOriginListName: string, originStory: Story) constructor(singleElement: KeyValuePair) constructor(){ // Trying to be smart here, this emulates the constructor inheritance found // in the original code, but only if otherList is an InkList. IIFE FTW. // @ts-ignore super((() => { if (arguments[0] instanceof InkList){ return arguments[0]; } else{ return undefined; } })()); if (arguments[0] instanceof InkList){ let otherList = arguments[0] as InkList; if (otherList._originNames) { this._originNames = otherList._originNames.slice(); } } else if (typeof arguments[0] === 'string'){ let singleOriginListName = arguments[0] as string; let originStory = arguments[1] /* as Story */; this.SetInitialOriginName(singleOriginListName); let def = originStory.listDefinitions.TryListGetDefinition(singleOriginListName, null); if (def.exists){ this.origins = [def.result]; } else{ throw new Error('InkList origin could not be found in story when constructing new list: ' + singleOriginListName); } } else if (typeof arguments[0] === 'object' && arguments[0].hasOwnProperty('Key') && arguments[0].hasOwnProperty('Value')){ let singleElement = arguments[0] as KeyValuePair; this.Add(singleElement.Key, singleElement.Value); } } // @ts-ignore public AddItem(itemOrItemName: InkListItem | string | null){ if (itemOrItemName instanceof InkListItem){ let item = itemOrItemName; if (item.originName == null) { this.AddItem(item.itemName); return; } if (this.origins === null) return throwNullException('this.origins'); for (let origin of this.origins) { if (origin.name == item.originName) { let intVal = origin.TryGetValueForItem(item, 0); if (intVal.exists) { this.Add(item, intVal.result); return; } else { throw new Error('Could not add the item ' + item + " to this list because it doesn't exist in the original list definition in ink."); } } } throw new Error("Failed to add item to list because the item was from a new list definition that wasn't previously known to this list. Only items from previously known lists can be used, so that the int value can be found."); } else { let itemName = itemOrItemName as string | null; let foundListDef: ListDefinition | null = null; if (this.origins === null) return throwNullException('this.origins'); for (let origin of this.origins) { if (itemName === null) return throwNullException('itemName'); if (origin.ContainsItemWithName(itemName)) { if (foundListDef != null) { throw new Error('Could not add the item ' + itemName + ' to this list because it could come from either ' + origin.name + ' or ' + foundListDef.name); } else { foundListDef = origin; } } } if (foundListDef == null) throw new Error('Could not add the item ' + itemName + " to this list because it isn't known to any list definitions previously associated with this list."); let item = new InkListItem(foundListDef.name, itemName); let itemVal = foundListDef.ValueForItem(item); this.Add(item, itemVal); } } public ContainsItemNamed(itemName: string | null){ for (let [key] of this) { let item = InkListItem.fromSerializedKey(key); if (item.itemName == itemName) return true; } return false; } public ContainsKey(key: InkListItem){ return this.has(key.serialized()); } public Add(key: InkListItem, value: number){ let serializedKey = key.serialized(); if (this.has(serializedKey)) { // Throw an exception to match the C# behavior. throw new Error(`The Map already contains an entry for ${key}`); } this.set(serializedKey, value); } public Remove(key: InkListItem){ return this.delete(key.serialized()); } get Count(){ return this.size; } get originOfMaxItem(): ListDefinition | null{ if (this.origins == null) return null; let maxOriginName = this.maxItem.Key.originName; let result = null; this.origins.every((origin)=>{ if (origin.name == maxOriginName){ result = origin; return false; } else return true; }); return result; } get originNames(): string[]{ if (this.Count > 0) { if (this._originNames == null && this.Count > 0) this._originNames = []; else { if (!this._originNames) this._originNames = []; this._originNames.length = 0; } for (let [key] of this) { let item = InkListItem.fromSerializedKey(key); if (item.originName === null) return throwNullException('item.originName'); this._originNames.push(item.originName); } } return this._originNames as string[]; } public SetInitialOriginName(initialOriginName: string){ this._originNames = [initialOriginName]; } public SetInitialOriginNames(initialOriginNames: string[]){ if (initialOriginNames == null) this._originNames = null; else this._originNames = initialOriginNames.slice();// store a copy } get maxItem(){ let max: KeyValuePair = { Key: InkListItem.Null, Value: 0, }; for (let [key, value] of this) { let item = InkListItem.fromSerializedKey(key); if (max.Key.isNull || value > max.Value) max = { Key: item, Value: value }; } return max; } get minItem(){ let min: KeyValuePair = { Key: InkListItem.Null, Value: 0, }; for (let [key, value] of this) { let item = InkListItem.fromSerializedKey(key); if (min.Key.isNull || value < min.Value) { min = { Key: item, Value: value }; } } return min; } get inverse(){ let list = new InkList(); if (this.origins != null) { for (let origin of this.origins) { for (let [key, value] of origin.items) { let item = InkListItem.fromSerializedKey(key); if (!this.ContainsKey(item)) list.Add(item, value); } } } return list; } get all(){ let list = new InkList(); if (this.origins != null) { for(let origin of this.origins) { for (let [key, value] of origin.items) { let item = InkListItem.fromSerializedKey(key); list.set(item.serialized(), value); } } } return list; } public Union(otherList: InkList){ let union = new InkList(this); for(let [key, value] of otherList) { union.set(key, value); } return union; } public Intersect(otherList: InkList){ let intersection = new InkList(); for(let [key, value] of this) { if (otherList.has(key)) intersection.set(key, value); } return intersection; } public Without(listToRemove: InkList){ let result = new InkList(this); for(let [key] of listToRemove) { result.delete(key); } return result; } public Contains(otherList: InkList){ for(let [key] of otherList) { if (!this.has(key)) return false; } return true; } public GreaterThan(otherList: InkList){ if (this.Count == 0) return false; if (otherList.Count == 0) return true; return this.minItem.Value > otherList.maxItem.Value; } public GreaterThanOrEquals(otherList: InkList){ if (this.Count == 0) return false; if (otherList.Count == 0) return true; return this.minItem.Value >= otherList.minItem.Value && this.maxItem.Value >= otherList.maxItem.Value; } public LessThan(otherList: InkList){ if (otherList.Count == 0) return false; if (this.Count == 0) return true; return this.maxItem.Value < otherList.minItem.Value; } public LessThanOrEquals(otherList: InkList){ if (otherList.Count == 0) return false; if (this.Count == 0) return true; return this.maxItem.Value <= otherList.maxItem.Value && this.minItem.Value <= otherList.minItem.Value; } public MaxAsList(){ if (this.Count > 0) return new InkList(this.maxItem); else return new InkList(); } public MinAsList(){ if (this.Count > 0) return new InkList(this.minItem); else return new InkList(); } public ListWithSubRange(minBound: any, maxBound: any) { if (this.Count == 0) return new InkList(); let ordered = this.orderedItems; let minValue = 0; let maxValue = Number.MAX_SAFE_INTEGER; if (Number.isInteger(minBound)) { minValue = minBound; } else { if (minBound instanceof InkList && minBound.Count > 0 ) minValue = minBound.minItem.Value; } if (Number.isInteger(maxBound)) { maxValue = maxBound; } else { if (minBound instanceof InkList && (minBound).Count > 0) maxValue = maxBound.maxItem.Value; } let subList = new InkList(); subList.SetInitialOriginNames(this.originNames); for (let item of ordered) { if (item.Value >= minValue && item.Value <= maxValue ) { subList.Add(item.Key, item.Value); } } return subList; } public Equals(otherInkList: InkList){ if (otherInkList instanceof InkList === false) return false; if (otherInkList.Count != this.Count) return false; for(let [key] of this) { if (!otherInkList.has(key)) return false; } return true; } // GetHashCode not implemented get orderedItems() { // List> let ordered = new Array>(); for(let [key, value] of this) { let item = InkListItem.fromSerializedKey(key); ordered.push({ Key: item, Value: value }); } ordered.sort((x, y) => { if (x.Key.originName === null) { return throwNullException('x.Key.originName'); } if (y.Key.originName === null) { return throwNullException('y.Key.originName'); } if (x.Value == y.Value) { return x.Key.originName.localeCompare(y.Key.originName); } else { // TODO: refactor this bit into a numberCompareTo method? if (x.Value < y.Value) return -1; return x.Value > y.Value ? 1 : 0; } }); return ordered; } public toString(){ let ordered = this.orderedItems; let sb = new StringBuilder(); for (let i = 0; i < ordered.length; i++) { if (i > 0) sb.Append(', '); let item = ordered[i].Key; if (item.itemName === null) return throwNullException('item.itemName'); sb.Append(item.itemName); } return sb.toString(); } // casting a InkList to a Number, for somereason, actually gives a number. // This messes up the type detection when creating a Value from a InkList. // Returning NaN here prevents that. public valueOf(){ return NaN; } } /** * In the original C# code, `InkListItem` was defined as value type, meaning * that two `InkListItem` would be considered equal as long as they held the * same values. This doesn't hold true in Javascript, as `InkListItem` is a * reference type (Javascript doesn't allow the creation of custom value types). * * The key equality of Map objects is based on the "SameValueZero" algorithm; * since `InkListItem` is a value type, two keys will only be considered * equal if they are, in fact, the same object. As we are trying to emulate * the original behavior as close as possible, this will lead to unforeseen * side effects. * * In order to have a key equality based on value semantics, we'll convert * `InkListItem` to a valid string representation and use this representation * as a key (strings are value types in Javascript). Rather than using the * type `string` directly, we'll alias it to `SerializedInkListItem` and use * this type as the key for our Map-based `InkList`. * * Reducing `InkListItem` to a JSON representation would not be bulletproof * in the general case, but for our needs it works well. The major downside of * this method is that we will have to to reconstruct the original `InkListItem` * every time we'll need to access its properties. */ export type SerializedInkListItem = string; /** * An interface inherited by `InkListItem`, defining exposed * properties. It's mainly used when deserializing a `InkListItem` from its * key (`SerializedInkListItem`) */ interface IInkListItem{ readonly originName: string | null; readonly itemName: string | null; } export interface KeyValuePair { Key: K; Value: V; }