roberthead/head_music

View on GitHub
lib/head_music/letter_name.rb

Summary

Maintainability
A
25 mins
Test Coverage
# frozen_string_literal: true

# Music has seven lette names that are used to identify pitches and pitch classes.
class HeadMusic::LetterName
  NAMES = %w[C D E F G A B].freeze

  NATURAL_PITCH_CLASS_NUMBERS = {
    "C" => 0,
    "D" => 2,
    "E" => 4,
    "F" => 5,
    "G" => 7,
    "A" => 9,
    "B" => 11
  }.freeze

  def self.all
    NAMES.map { |letter_name| get(letter_name) }
  end

  def self.get(identifier)
    from_name(identifier) || from_pitch_class(identifier)
  end

  def self.from_name(name)
    @letter_names ||= {}
    name = name.to_s.first.upcase
    @letter_names[name] ||= new(name) if NAMES.include?(name)
  end

  def self.from_pitch_class(pitch_class)
    @letter_names ||= {}
    return nil if pitch_class.to_s == pitch_class

    pitch_class = pitch_class.to_i % 12
    name = NAMES.detect { |candidate| pitch_class == NATURAL_PITCH_CLASS_NUMBERS[candidate] }
    name ||= HeadMusic::PitchClass::SHARP_SPELLINGS[pitch_class].first
    @letter_names[name] ||= new(name) if NAMES.include?(name)
  end

  attr_reader :name

  delegate :to_s, to: :name
  delegate :to_sym, to: :name
  delegate :to_i, to: :pitch_class

  def initialize(name)
    @name = name
  end

  def pitch_class
    HeadMusic::PitchClass.get(NATURAL_PITCH_CLASS_NUMBERS[name])
  end

  def ==(other)
    to_s == other.to_s
  end

  def position
    NAMES.index(to_s) + 1
  end

  def steps_up(num)
    HeadMusic::LetterName.get(series_ascending[num % NAMES.length])
  end

  def steps_down(num)
    HeadMusic::LetterName.get(series_descending[num % NAMES.length])
  end

  def steps_to(other, direction = :ascending)
    other = HeadMusic::LetterName.get(other)
    other_position = other.position
    if direction == :descending
      other_position -= NAMES.length if other_position > position
      position - other_position
    else
      other_position += NAMES.length if other_position < position
      other_position - position
    end
  end

  def series_ascending
    @series_ascending ||= begin
      series = NAMES
      series = series.rotate while series.first != to_s
      series
    end
  end

  def series_descending
    @series_descending ||= begin
      series = NAMES.reverse
      series = series.rotate while series.first != to_s
      series
    end
  end

  private_class_method :new
end