import AssertionError from "../error/AssertionError"; import EtudeError from "../error/EtudeError"; import EtudeParser from "../util/EtudeParser"; import Exceptional from "../util/Exceptional"; import ImmutablePrioritySet from "../util/ImmutablePrioritySet"; import MathUtil from "../util/MathUtil"; import StreamUtil from "../util/StreamUtil"; import Accidental from "./Accidental"; import KeySignature from "./KeySignature"; import Letter from "./Letter"; import MusicConstants from "./MusicConstants"; import Policy from "./Policy"; import Scale from "./Scale"; export default class Key { private accidental: Exceptional; constructor(private letter: Letter, parameter: Accidental | Exceptional = Exceptional.empty()) { if (parameter instanceof Accidental) { this.accidental = Exceptional.of(parameter); } else if (parameter instanceof Exceptional) { this.accidental = parameter; } else { this.accidental = Exceptional.empty(); } } public step(amount: number, policies: ImmutablePrioritySet = Policy.DEFAULT_PRIORITY): Exceptional { return Key.fromOffset(MathUtil.floorMod(this.getOffset() + amount, MusicConstants.KEYS_IN_OCTAVE), policies); } public apply(keySignature: KeySignature): Key { const accidental: Exceptional = keySignature.getAccidentalFor(this.letter); if (this.accidental === accidental) { return this; } return new Key(this.letter, accidental); } public removeAccidental(): Key { if (!this.hasAccidental()) { return this; } return new Key(this.letter); } public natural(): Key { if (this.isNatural()) { return this; } return new Key(this.letter, Accidental.NATURAL); } public sharp(): Key { if (this.isSharp()) { return this; } return new Key(this.letter, Accidental.SHARP); } public doubleSharp(): Key { if (this.isDoubleSharp()) { return this; } return new Key(this.letter, Accidental.DOUBLE_SHARP); } public tripleSharp(): Key { if (this.isTripleSharp()) { return this; } return new Key(this.letter, Accidental.TRIPLE_SHARP); } public flat(): Key { if (this.isFlat()) { return this; } return new Key(this.letter, Accidental.FLAT); } public doubleFlat(): Key { if (this.isDoubleFlat()) { return this; } return new Key(this.letter, Accidental.DOUBLE_FLAT); } public tripleFlat(): Key { if (this.isTripleFlat()) { return this; } return new Key(this.letter, Accidental.TRIPLE_FLAT); } public hasAccidental(): boolean { return this.accidental.isPresent(); } public isNatural(): boolean { return this.accidental.orElse(null) === Accidental.NATURAL; } public isSharp(): boolean { return this.accidental.orElse(null) === Accidental.SHARP; } public isDoubleSharp(): boolean { return this.accidental.orElse(null) === Accidental.DOUBLE_SHARP; } public isTripleSharp(): boolean { return this.accidental.orElse(null) === Accidental.TRIPLE_SHARP; } public isFlat(): boolean { return this.accidental.orElse(null) === Accidental.FLAT; } public isDoubleFlat(): boolean { return this.accidental.orElse(null) === Accidental.DOUBLE_FLAT; } public isTripleFlat(): boolean { return this.accidental.orElse(null) === Accidental.TRIPLE_FLAT; } public getEnharmonicEquivalent(parameter: Letter | ImmutablePrioritySet): Exceptional { if (parameter instanceof Letter) { const letter = parameter as Letter; return Exceptional .ofNullable(letter) .flatMap(l => { if (this.letter === l) { return Exceptional.of(this); } const targetOffset: number = this.getOffset(); const initialOffset: number = letter.getOffset(); let accidentalOffset: number = targetOffset - initialOffset; // assuming key is above, check if some sharp can offset enough if (accidentalOffset > Accidental.TRIPLE_SHARP.getOffset()) { // assumption was wrong, now assume key is below accidentalOffset -= MusicConstants.KEYS_IN_OCTAVE; // check if some flat can offset enough if (accidentalOffset < Accidental.TRIPLE_FLAT.getOffset()) { return Exceptional.empty(EtudeError.forInvalid(Key, accidentalOffset)); } } else if (accidentalOffset < Accidental.TRIPLE_FLAT.getOffset()) { accidentalOffset += MusicConstants.KEYS_IN_OCTAVE; if (accidentalOffset > Accidental.TRIPLE_SHARP.getOffset()) { return Exceptional.empty(EtudeError.forInvalid(Key, accidentalOffset)); } } const accidental: Exceptional = Accidental.fromOffset(accidentalOffset); return Exceptional.of(new Key(letter, accidental)); }); } else if (parameter instanceof ImmutablePrioritySet) { const policies: ImmutablePrioritySet = parameter as ImmutablePrioritySet; // all keys that are enharmonic equivalents const keys: Key[] = Letter .stream() .limit(Letter.size) .map(l => this.getEnharmonicEquivalent(l)) .filter(e => e.isPresent()) .map(e => e.get()); // find the first policy that tests true for some enharmonic key and return that key const value: Key = StreamUtil .fromIterator(policies.iterator()) .map(policy => { const value = keys.find(k => policy(k)); return Exceptional.ofNullable(value); }) .filter(e => e.isPresent()) .map(e => e.get()) .limit(1) [0]; return Exceptional .ofNullable(value) .withException(EtudeError.forInvalid(Key, "unable to find an enharmonic equivalent for the given policies")); } } public static isEnharmonic(a: Key, b: Key): boolean { return a.getOffset() === b.getOffset(); } public static fromOffset(offset: number, policies: ImmutablePrioritySet = Policy.DEFAULT_PRIORITY): Exceptional { if (policies.size === 0) { throw EtudeError.forIllegalArgument(Key, policies, "should not be empty"); } let letter: Letter; let accidental: Accidental = null; // determine key without taking into account policies // maintain order of cases for fall throughs to function correctly switch (MathUtil.floorMod(offset, MusicConstants.KEYS_IN_OCTAVE)) { case 11: letter = Letter.B; break; case 10: accidental = Accidental.SHARP; // fall through case 9: letter = Letter.A; break; case 8: accidental = Accidental.SHARP; // fall through case 7: letter = Letter.G; break; case 6: accidental = Accidental.SHARP; // fall through case 5: letter = Letter.F; break; case 4: letter = Letter.E; break; case 3: accidental = Accidental.SHARP; // fall through case 2: letter = Letter.D; break; case 1: accidental = Accidental.SHARP; // fall through case 0: letter = Letter.C; break; default: throw new AssertionError("Invalid offset: " + offset); } return new Key(letter, accidental).getEnharmonicEquivalent(policies); } public getOffset(): number { return MathUtil.floorMod(this.letter.getOffset() + this.accidental.map(a => a.getOffset()).orElse(0), MusicConstants.KEYS_IN_OCTAVE); } public static fromString(keyString: string): Exceptional { return EtudeParser .of(keyString) .filter(o => o != null, EtudeError.forNull(Key)) .filter(s => s.trim().length !== 0, EtudeError.forInvalid(Key, keyString, "empty string")) .parse(s => Letter.fromChar(s.charAt(0))) .parse(s => Exceptional.of(Accidental.fromString(s.length === 1 ? null : s.substring(1)))) .get(a => new Key(a[0] as Letter, a[1] as Exceptional)); } public toString(): string { return this.letter.toString() + this.accidental.map(a => a.toString()).orElse(""); } public equals(other: any): boolean { if (!(other instanceof Key)) { return false; } if (other === this) { return true; } let otherKey = other as Key; return this.letter === otherKey.getLetter() && this.accidental === otherKey.getAccidental(); } public getLetter(): Letter { return this.letter; } public getAccidental(): Exceptional { return this.accidental; } }