martijnversluis/ChordSheetJS

View on GitHub
src/chord.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { parse } from './parser/chord/peg_parser';
import Key from './key';
import { isMinor, normalizeChordSuffix } from './utilities';
import ChordParsingError from './chord_parsing_error';
import {
  ChordType,
  Modifier,
  NUMERAL,
  NUMERIC,
  SYMBOL,
  SOLFEGE,
} from './constants';

interface ChordProperties {
  root?: Key | null;
  suffix?: string | null;
  bass?: Key | null;
}

/**
 * Represents a Chord, consisting of a root, suffix (quality) and bass
 */
class Chord implements ChordProperties {
  bass: Key | null;

  root: Key | null;

  suffix: string | null;

  /**
   * Tries to parse a chord string into a chord
   * Any leading or trailing whitespace is removed first, so a chord like `  \n  E/G# \r ` is valid.
   * @param chordString the chord string, eg `Esus4/G#` or `1sus4/#3`.
   * @returns {Chord|null}
   */
  static parse(chordString: string): Chord | null {
    try {
      return this.parseOrFail(chordString);
    } catch (_error) {
      return null;
    }
  }

  static parseOrFail(chordString: string): Chord {
    const trimmedChord = chordString.trim();

    try {
      const ast = parse(trimmedChord);
      return new Chord(ast);
    } catch (error) {
      const errorObj = error as Error;
      throw new ChordParsingError(`Failed parsing '${trimmedChord}': ${errorObj.message}`);
    }
  }

  /**
   * Returns a deep copy of the chord
   * @returns {Chord}
   */
  clone(): Chord {
    return this.set({});
  }

  /**
   * Converts the chord to a chord symbol, using the supplied key as a reference.
   * For example, a numeric chord `#4` with reference key `E` will return the chord symbol `A#`.
   * When the chord is already a chord symbol, it will return a clone of the object.
   * @param {Key|string|null} [referenceKey=null] the reference key. The key is required when converting a
   * numeric or numeral.
   * @returns {Chord} the chord symbol
   */
  toChordSymbol(referenceKey: Key | string | null = null): Chord {
    if (this.isChordSymbol()) {
      return this.clone();
    }

    const keyObj = Key.wrapOrFail(referenceKey);

    let chordSymbolChord = new Chord({
      suffix: this.suffix ? normalizeChordSuffix(this.suffix) : null,
      root: this.root?.toChordSymbol(keyObj) || null,
      bass: this.bass?.toChordSymbol(keyObj) || null,
    });

    if (this.root?.isMinor()) {
      chordSymbolChord = chordSymbolChord.makeMinor();
    }

    chordSymbolChord = chordSymbolChord.normalize(referenceKey);
    return chordSymbolChord;
  }

  /**
   * Converts the chord to a chord symbol string, using the supplied key as a reference.
   * For example, a numeric chord `#4` with reference key `E` will return the chord symbol `A#`.
   * When the chord is already a chord symbol, it will return a string version of the chord.
   * @param {Key|string|null} [referenceKey=null] the reference key. The key is required when converting a
   * numeric or numeral.
   * @returns {string} the chord symbol string
   * @see {toChordSymbol}
   */
  toChordSymbolString(referenceKey: Key | string | null = null): string {
    return this.toChordSymbol(referenceKey).toString();
  }

  /**
   * Determines whether the chord is a chord symbol
   * @returns {boolean}
   */
  isChordSymbol(): boolean {
    return this.is(SYMBOL);
  }

  /**
   * Converts the chord to a chord solfege, using the supplied key as a reference.
   * For example, a numeric chord `#4` with reference key `Mi` will return the chord symbol `La#`.
   * When the chord is already a chord solfege, it will return a clone of the object.
   * @param {Key|string|null} [referenceKey=null] the reference key. The key is required when converting a
   * numeric or numeral.
   * @returns {Chord} the chord solfege
   */
  toChordSolfege(referenceKey: Key | string | null = null): Chord {
    if (this.isChordSolfege()) {
      return this.clone();
    }

    const keyObj = Key.wrapOrFail(referenceKey);

    let chordSolfegeChord = new Chord({
      suffix: this.suffix ? normalizeChordSuffix(this.suffix) : null,
      root: this.root?.toChordSolfege(keyObj) || null,
      bass: this.bass?.toChordSolfege(keyObj) || null,
    });

    if (this.root?.isMinor()) {
      chordSolfegeChord = chordSolfegeChord.makeMinor();
    }

    chordSolfegeChord = chordSolfegeChord.normalize(referenceKey);
    return chordSolfegeChord;
  }

  /**
   * Converts the chord to a chord solfege string, using the supplied key as a reference.
   * For example, a numeric chord `#4` with reference key `E` will return the chord solfege `A#`.
   * When the chord is already a chord solfege, it will return a string version of the chord.
   * @param {Key|string|null} [referenceKey=null] the reference key. The key is required when converting a
   * numeric or numeral.
   * @returns {string} the chord solfege string
   * @see {toChordSolfege}
   */
  toChordSolfegeString(referenceKey: Key | string | null = null): string {
    return this.toChordSolfege(referenceKey).toString();
  }

  /**
   * Determines whether the chord is a chord solfege
   * @returns {boolean}
   */
  isChordSolfege(): boolean {
    return this.is(SOLFEGE);
  }

  /**
   * Converts the chord to a numeric chord, using the supplied key as a reference.
   * For example, a chord symbol A# with reference key E will return the numeric chord #4.
   * @param {Key|string|null} [referenceKey=null] the reference key. The key is required when converting a chord symbol
   * @returns {Chord} the numeric chord
   */
  toNumeric(referenceKey: Key | string | null = null): Chord {
    if (this.isNumeric()) {
      return this.clone();
    }

    if (this.isNumeral()) {
      return this.transform((key) => key.toNumeric());
    }

    const keyObj: Key | null = Key.wrap(referenceKey);

    return new Chord({
      suffix: normalizeChordSuffix(this.suffix),
      root: this.root?.toNumeric(keyObj) || null,
      bass: this.bass?.toNumeric(keyObj) || null,
    });
  }

  /**
   * Converts the chord to a numeral chord, using the supplied key as a reference.
   * For example, a chord symbol A# with reference key E will return the numeral chord #IV.
   * @param {Key|string|null} [referenceKey=null] the reference key. The key is required when converting a chord symbol
   * @returns {Chord} the numeral chord
   */
  toNumeral(referenceKey: Key | string | null = null): Chord {
    if (this.isNumeral()) {
      return this.clone();
    }

    if (this.isNumeric()) {
      return this.transform((key) => key.toNumeral());
    }

    const keyObj = Key.wrapOrFail(referenceKey);

    return new Chord({
      suffix: normalizeChordSuffix(this.suffix),
      root: (keyObj && this.root) ? this.root.toNumeral(keyObj) : null,
      bass: this.bass?.toNumeral(keyObj) || null,
    });
  }

  /**
   * Converts the chord to a numeral chord string, using the supplied kye as a reference.
   * For example, a chord symbol A# with reference key E will return the numeral chord #4.
   * @param {Key|string|null} [referenceKey=null] the reference key. The key is required when converting a chord symbol
   * @returns {string} the numeral chord string
   * @see {toNumeral}
   */
  toNumeralString(referenceKey: Key | string | null = null): string {
    return this.toNumeral(referenceKey).toString();
  }

  /**
   * Determines whether the chord is numeric
   * @returns {boolean}
   */
  isNumeric(): boolean {
    return this.is(NUMERIC);
  }

  /**
   * Converts the chord to a numeric chord string, using the supplied kye as a reference.
   * For example, a chord symbol A# with reference key E will return the numeric chord #4.
   * @param {Key|string|null} [referenceKey=null] the reference key. The key is required when converting a chord symbol
   * @returns {string} the numeric chord string
   * @see {toNumeric}
   */
  toNumericString(referenceKey: Key | string | null = null): string {
    return this.toNumeric(referenceKey).toString();
  }

  /**
   * Determines whether the chord is a numeral
   * @returns {boolean}
   */
  isNumeral(): boolean {
    return this.is(NUMERAL);
  }

  /**
   * Converts the chord to a string, eg `Esus4/G#` or `1sus4/#3`
   * @param {Object} [configuration={}] options
   * @param {boolean} [configuration.useUnicodeModifier=false] Whether or not to use unicode modifiers.
   * This will make `#` (sharp) look like `♯` and `b` (flat) look like `♭`
   * @returns {string} the chord string
   */
  toString({ useUnicodeModifier = false } = {}): string {
    let chordString = '';
    const suffix = this.suffix || '';
    const showMinor = suffix[0] !== 'm';

    if (this.root) chordString = this.root.toString({ showMinor, useUnicodeModifier }) + suffix;
    if (this.bass) return `${chordString}/${this.bass.toString({ useUnicodeModifier })}`;

    return chordString;
  }

  /**
   * Normalizes the chord root and bass notes:
   * - Fab becomes Mi
   * - Dob becomes Si
   * - Si# becomes Do
   * - Mi# becomes Fa
   * - Fb becomes E
   * - Cb becomes B
   * - B# becomes C
   * - E# becomes F
   * - 4b becomes 3
   * - 1b becomes 7
   * - 7# becomes 1
   * - 3# becomes 4
   *
   * Besides that it normalizes the suffix if `normalizeSuffix` is `true`.
   * For example, `sus2` becomes `2`, `sus4` becomes `sus`.
   * All suffix normalizations can be found in `src/normalize_mappings/suffix-mapping.txt`.
   *
   * When the chord is minor, bass notes are normalized off of the relative major
   * of the root note. For example, `Em/A#` becomes `Em/Bb`.
   * @param {Key|string} [key=null] the key to normalize to
   * @param {Object} [options={}] options
   * @param {boolean} [options.normalizeSuffix=true] whether to normalize the chord suffix after transposing
   * @returns {Chord} the normalized chord
   */
  normalize(key: Key | string | null = null, { normalizeSuffix = true } = {}): Chord {
    const suffix = normalizeSuffix ? normalizeChordSuffix(this.suffix) : this.suffix;
    let normalizedRoot = this.root;

    if (this.root) {
      normalizedRoot = this.root.normalize();
      if (key) normalizedRoot = normalizedRoot.normalizeEnharmonics(key);
    }

    return this.set({
      suffix,
      root: normalizedRoot,
      bass: this.bass ? this.bass.normalize().normalizeEnharmonics(normalizedRoot) : null,
    });
  }

  /**
   * Switches to the specified modifier
   * @param newModifier the modifier to use: `'#'` or `'b'`
   * @returns {Chord} the new, changed chord
   */
  useModifier(newModifier: Modifier): Chord {
    return this.transform((key) => key.useModifier(newModifier));
  }

  /**
   * Transposes the chord up by 1 semitone. Eg. A becomes A#, Eb becomes E
   * @returns {Chord} the new, transposed chord
   */
  transposeUp(): Chord {
    return this.transform((key) => key.transposeUp());
  }

  /**
   * Transposes the chord down by 1 semitone. Eg. A# becomes A, E becomes Eb
   * @returns {Chord} the new, transposed chord
   */
  transposeDown(): Chord {
    return this.transform((key) => key.transposeDown());
  }

  /**
   * Transposes the chord by the specified number of semitones
   * @param delta de number of semitones
   * @returns {Chord} the new, transposed chord
   */
  transpose(delta: number): Chord {
    return this.transform((key) => key.transpose(delta));
  }

  constructor(
    {
      base = null,
      modifier = null,
      suffix = null,
      bassBase = null,
      bassModifier = null,
      root = null,
      bass = null,
      chordType = null,
    }: {
      base?: string | number | null,
      modifier?: Modifier | null,
      suffix?: string | null,
      bassBase?: string | number | null,
      bassModifier?: Modifier | null,
      root?: Key | null,
      bass?: Key | null,
      chordType?: ChordType | null,
    },
  ) {
    this.suffix = suffix || null;
    this.root = Chord.determineRoot({
      root, base, modifier, suffix, chordType,
    });
    this.bass = Chord.determineBass({
      bass, bassBase, bassModifier, chordType,
    });
  }

  equals(otherChord: Chord): boolean {
    return this.suffix === otherChord.suffix &&
      Key.equals(this.root, otherChord.root) &&
      Key.equals(this.bass, otherChord.bass);
  }

  static determineRoot(
    {
      root,
      base,
      modifier,
      suffix,
      chordType,
    }: {
      root: Key | null,
      base: string | number | null,
      modifier: Modifier | null,
      suffix: string | null,
      chordType: ChordType | null,
    },
  ): Key | null {
    if (root) return root;
    if (!base) return null;
    if (!chordType) throw new Error('Can\'t resolve at this point without a chord type');

    return Key.resolve({
      key: base,
      keyType: chordType,
      minor: isMinor(base, chordType, suffix),
      modifier,
    });
  }

  static determineBass(
    {
      bass,
      bassBase,
      bassModifier,
      chordType,
    }: {
      bass: Key | null,
      bassBase: string | number | null,
      bassModifier: Modifier | null,
      chordType: ChordType | null,
    },
  ): Key | null {
    if (bass) return bass;
    if (!bassBase) return null;
    if (!chordType) throw new Error('Can\'t resolve at this point without a chord type');

    return Key.resolve({
      key: bassBase,
      modifier: bassModifier || null,
      minor: false,
      keyType: chordType,
    });
  }

  isMinor(): boolean {
    return this.root?.isMinor() || false;
  }

  makeMinor(): Chord {
    return this.set({
      root: this.root?.makeMinor() || null,
    });
  }

  set(properties: ChordProperties): Chord {
    return new Chord(
      {
        root: this.root?.clone() || null,
        suffix: this.suffix,
        bass: this.bass?.clone() || null,
        ...properties,
      },
    );
  }

  private is(type: ChordType): boolean {
    return (!this.root || this.root.is(type)) && (!this.bass || this.bass.is(type));
  }

  private transform(transformFunc: (_key: Key) => Key): Chord {
    return this.set({
      root: this.root ? transformFunc(this.root) : null,
      bass: this.bass ? transformFunc(this.bass) : null,
    });
  }
}

export default Chord;