diff --git a/index.ts b/index.ts index 4a3b7e8..1cd1dea 100644 --- a/index.ts +++ b/index.ts @@ -1 +1,2 @@ export * as result from './src/result' +export * as option from './src/option' diff --git a/src/option.ts b/src/option.ts new file mode 100644 index 0000000..3701bc2 --- /dev/null +++ b/src/option.ts @@ -0,0 +1,113 @@ +import logger from 'log' +import {Result} from './result' + +const log = logger('option') + +export class Option { + // ***************************************************************************************************************** + // Constructors + private constructor( + private readonly data: T | null, + private readonly _state: State, + ) {} + public static some(data: T) : Option { + return new Option(data, State.Some) + } + public static none() : Option { + return new Option(null, State.None) + } + + // ***************************************************************************************************************** + // some + public is_some() : boolean { + return this._state === State.Some + } + public is_some_and(f: (data: T) => boolean) : boolean { + return this._state === State.Some && f(this.data as T) + } + public expect(msg: string) : T { + log.trace("expect") + + if (this._state === State.Some) { + return this.data as T + } + + log.warn(msg) + throw new Error(msg) + } + public unwrap() : T { + log.trace("unwrap") + return this.expect("called unwrap on a None value") + } + public unwrap_or(default_: T) : T { + if (this._state === State.Some) return this.data as T + return default_ + } + public unwrap_or_else(f: () => T) : T { + if (this._state === State.Some) return this.data as T + return f() + } + + // ***************************************************************************************************************** + // none + public is_none() : boolean { + return this._state === State.None + } + + // ***************************************************************************************************************** + // map + public map(f: (data: T) => U) : Option { + if (this._state === State.Some) return Option.some(f(this.data as T)) + return Option.none() + } + public map_or(default_: U, f: (data: T) => U) : U { + if (this._state === State.Some) return f(this.data as T) + return default_ + } + public map_or_else(default_: () => U, f: (data: T) => U) : U { + if (this._state === State.Some) return f(this.data as T) + return default_() + } + public ok_or(err: E) : Result { + if (this._state === State.Some) return Result.ok(this.data as T) + return Result.error(err) + } + public ok_or_else(err: () => E) : Result { + if (this._state === State.Some) return Result.ok(this.data as T) + return Result.error(err()) + } + public and(optb: Option) : Option { + if (this._state === State.Some) return optb + return Option.none() + } + public and_then(f: (data: T) => Option) : Option { + if (this._state === State.Some) return f(this.data as T) + return Option.none() + } + public filter(f: (data: T) => boolean) : Option { + if (this._state === State.Some && f(this.data as T)) return this + return Option.none() + } + public or(optb: Option) : Option { + if (this._state === State.Some) return this + return optb + } + public or_else(f: () => Option) : Option { + if (this._state === State.Some) return this + return f() + } + public xor(optb: Option) : Option { + if (this._state === optb._state) return Option.none() + else if (this._state === State.Some) return this + return optb + } + + public get state() : State { + return this._state + } +} + +export enum State { + Some, + None, +} diff --git a/test/option.test.ts b/test/option.test.ts new file mode 100644 index 0000000..dc51066 --- /dev/null +++ b/test/option.test.ts @@ -0,0 +1,70 @@ +import {expect, test} from 'bun:test' + +import {Option, State} from 'option' + +test('Base case Some', () => { + const value = 12 + const opt = Option.some(value) + + const value2 = 1 + const opt2 = Option.some(value2) + + const handler_throw = () => { + throw new Error("Shouldn't happen") + } + + expect(opt.is_some()).toBeTrue() + expect(opt.is_some_and(data => data === value)).toBeTrue() + expect(opt.expect("Shouldn't happen")).toBe(value) + expect(opt.unwrap()).toBe(value) + expect(opt.unwrap_or(0)).toBe(value) + expect(opt.unwrap_or_else(handler_throw)).toBe(value) + expect(opt.is_none()).toBeFalse() + expect(opt.map(data => data + 1).unwrap()).toBe(value + 1) + expect(opt.map_or(0, data => data + 1)).toBe(value + 1) + expect(opt.map_or_else(handler_throw, data => data + 1)).toBe(value + 1) + expect(opt.ok_or("Shouldn't happen").unwrap()).toBe(value) + expect(opt.ok_or_else(handler_throw).unwrap()).toBe(value) + expect(opt.and(opt2).unwrap()).toBe(value2) + expect(opt.and_then(data => Option.some(data + 1)).unwrap()).toBe(value + 1) + expect(opt.filter(data => data === value).unwrap()).toBe(value) + expect(opt.or(opt2).unwrap()).toBe(value) + expect(opt.or_else(handler_throw).unwrap()).toBe(value) + + expect(opt.xor(Option.none()).unwrap()).toBe(value) + expect(opt.xor(opt2).is_none()).toBeTrue() + + expect(opt.state).toBe(State.Some) +}) +test('Base case None', () => { + const opt = Option.none() + const value2 = 12 + const opt2 = Option.some(value2) + + const handler_throw = () => { + throw new Error("Shouldn't happen") + } + + expect(opt.is_some()).toBeFalse() + expect(opt.is_some_and(handler_throw)).toBeFalse() + expect(() => opt.expect("Shouldn't happen")).toThrow() + expect(() => opt.unwrap()).toThrow() + expect(opt.unwrap_or(0)).toBe(0) + expect(opt.unwrap_or_else(() => 0)).toBe(0) + expect(opt.is_none()).toBeTrue() + expect(opt.map(handler_throw).is_none()).toBeTrue() + expect(opt.map_or(0, handler_throw)).toBe(0) + expect(opt.map_or_else(() => 0, handler_throw)).toBe(0) + expect(opt.ok_or(0).unwrap_err()).toBe(0) + expect(opt.ok_or_else(() => 0).unwrap_err()).toBe(0) + expect(opt.and(opt2).is_none()).toBeTrue() + expect(opt.and_then(handler_throw).is_none()).toBeTrue() + expect(opt.filter(handler_throw).is_none()).toBeTrue() + expect(opt.or(opt2).unwrap()).toBe(value2) + expect(opt.or_else(() => opt2).unwrap()).toBe(value2) + + expect(opt.xor(Option.none()).is_none()).toBeTrue() + expect(opt.xor(opt2).unwrap()).toBe(value2) + + expect(opt.state).toBe(State.None) +})