diff --git a/src/common/all_test.ts b/src/common/all_test.ts index ac3db47..4834b0b 100644 --- a/src/common/all_test.ts +++ b/src/common/all_test.ts @@ -3,7 +3,7 @@ import Buffer from './buffer.ts'; import Document from './document.ts'; import Editor from './editor.ts'; import { highlightToColor, HighlightType } from './highlight.ts'; -import _Option, { None, Some } from './option.ts'; +import Option, { None, Some } from './option.ts'; import Position from './position.ts'; import Row from './row.ts'; @@ -417,7 +417,19 @@ const EditorTest = { // ---------------------------------------------------------------------------- const OptionTest = { - // @TODO implement Option tests + 'Option.from()': () => { + assertTrue(Option.from(null).isNone()); + assertTrue(Option.from().isNone()); + assertEquivalent(Option.from(undefined), None); + + assertEquivalent(Option.from(Some('foo')), Some('foo')); + assertEquivalent(Some(Some('bar')), Some('bar')); + }, + '.toString': () => { + assertEquals(Some({}).toString(), 'Some ({})'); + assertEquals(Some([1, 2, 3]).toString(), 'Some ([1,2,3])'); + assertEquals(None.toString(), 'None'); + }, }; // ---------------------------------------------------------------------------- diff --git a/src/common/option.ts b/src/common/option.ts index 252a5e9..867e03f 100644 --- a/src/common/option.ts +++ b/src/common/option.ts @@ -7,8 +7,27 @@ enum OptionType { None = 'None', } +// ---------------------------------------------------------------------------- +// Typeguards to handle Some/None difference +// ---------------------------------------------------------------------------- + const isOption = (v: any): v is Option => v instanceof Option; +class OptionInnerNone { + public type: OptionType = OptionType.None; +} + +class OptionInnerSome { + public type: OptionType = OptionType.Some; + + constructor(public value: T) {} +} + +type OptionInnerType = OptionInnerNone | OptionInnerSome; + +const isSome = (v: OptionInnerType): v is OptionInnerSome => + 'value' in v && v.type === OptionType.Some; + /** * Rust-style optional type * @@ -18,50 +37,41 @@ export class Option { /** * The placeholder for the 'None' value type */ - private static _noneInstance: Option = new Option(); + private static _None: Option = new Option(null); /** * Is this a 'Some' or a 'None'? */ - private optionType: OptionType; + private readonly inner: OptionInnerType; - /** - * The value for the 'Some' type - */ - private value?: T; - - private constructor(v?: T | null) { - if (v !== undefined && v !== null) { - this.optionType = OptionType.Some; - this.value = v; - } else { - this.optionType = OptionType.None; - this.value = undefined; - } + private constructor(v?: T) { + this.inner = (v !== undefined && v !== null) + ? new OptionInnerSome(v) + : new OptionInnerNone(); } public static get None(): Option { - return > Option._noneInstance; + return Option._None; } - public static Some(v: X): Option { - return new Option(v); + public static Some(v: any): Option { + return Option.from(v); } - public static from(v: any): Option { + public static from(v?: any): Option { return (isOption(v)) ? Option.from(v.unwrap()) : new Option(v); } isSome(): boolean { - return this.optionType === OptionType.Some && this.value !== undefined; + return isSome(this.inner); } isNone(): boolean { - return this.optionType === OptionType.None; + return !this.isSome(); } isSomeAnd(fn: (a: T) => boolean): boolean { - return this.isSome() ? fn(this.unwrap()) : false; + return isSome(this.inner) ? fn(this.inner.value) : false; } isNoneAnd(fn: () => boolean): boolean { @@ -69,20 +79,20 @@ export class Option { } map(fn: (a: T) => U): Option { - return this.isSome() ? new Option(fn(this.unwrap())) : Option._noneInstance; + return isSome(this.inner) ? Option.from(fn(this.inner.value)) : Option.None; } mapOr(def: U, f: (a: T) => U): U { - return this.isSome() ? f(this.unwrap()) : def; + return isSome(this.inner) ? f(this.inner.value) : def; } mapOrElse(def: () => U, f: (a: T) => U): U { - return this.isSome() ? f(this.unwrap()) : def(); + return isSome(this.inner) ? f(this.inner.value) : def(); } unwrap(): T | never { - if (this.isSome() && this.value !== undefined) { - return this.value; + if (isSome(this.inner)) { + return this.inner.value; } console.error('None.unwrap()'); @@ -90,19 +100,19 @@ export class Option { } unwrapOr(def: T): T { - return this.isSome() ? this.unwrap() : def; + return isSome(this.inner) ? this.inner.value : def; } unwrapOrElse(f: () => T): T { - return this.isSome() ? this.unwrap() : f(); + return isSome(this.inner) ? this.inner.value : f(); } and(optb: Option): Option { - return this.isSome() ? optb : Option._noneInstance; + return isSome(this.inner) ? optb : Option.None; } andThen(f: (a: T) => Option): Option { - return this.isSome() ? f(this.unwrap()) : Option._noneInstance; + return isSome(this.inner) ? f(this.inner.value) : Option.None; } or(optb: Option): Option { @@ -114,10 +124,10 @@ export class Option { } toString(): string { - const innerValue = (this.value !== undefined) - ? JSON.stringify(this.value) + const innerValue = (isSome(this.inner)) + ? JSON.stringify(this.inner.value) : ''; - const prefix = this.optionType.valueOf(); + const prefix = this.inner.type.valueOf(); return (innerValue.length > 0) ? `${prefix} (${innerValue})` : prefix; }