Class: OutroRails::Theory::NashvilleNumber

Inherits:
Object
  • Object
show all
Defined in:
lib/outro_rails/theory/nashville_number.rb

Overview

Parses Nashville numbers ("1", "4", "2m", "b7", and with extensions riding along: "2m7", "57" is 5 with a dominant 7) and roman numerals ("IV", "ii", "V7", "bVII", "vii°"). The result is key-agnostic; Theory::Key#nashville resolves it to a chord.

Defined Under Namespace

Classes: ParseResult

Constant Summary collapse

ROMAN_NUMERALS =

Uppercase numerals in scale-degree order, indexed by degree - 1.

%w[I II III IV V VI VII].freeze
ROMAN_DEGREES =

Lowercase numeral => degree, derived so the two tables can't drift.

ROMAN_NUMERALS
.each_with_index
.to_h { |numeral, index| [ numeral.downcase, index + 1 ] }
.freeze
QUALITY_SUFFIXES =

Every accepted spelling of a quality suffix. "" maps to nil so the caller can fall back to the quality implied by numeral case.

{
  ""     => nil,
  "m"    => :minor,
  "min"  => :minor,
  "-"    => :minor,
  "maj"  => :major,
  "dim"  => :diminished,
  "\u00B0"    => :diminished,
  "o"    => :diminished,
  "aug"  => :augmented,
  "+"    => :augmented,
  "sus2" => :suspended_2,
  "sus4" => :suspended_4,
  "sus"  => :suspended_4,
  "5"    => :power
}.freeze
EXTENSION_SUFFIXES =

Extensions recognized after the degree/quality, matching ChordVocabulary::EXTENSIONS keys.

%w[maj7 dim7 6 7 9 11 13].freeze
ARABIC_PATTERN =

Splits a symbol into optional accidental, degree, and the suffix carrying quality and extension - arabic ("b7m") and roman ("bviim").

/\A(?<accidental>[b#]?)(?<degree>[1-7])(?<rest>.*)\z/
ROMAN_PATTERN =
/\A(?<accidental>[b#]?)(?<numeral>[ivIV]+)(?<rest>.*)\z/
QUALITY_DISPLAY =

Display suffix per quality for arabic Nashville numbers; qualities not listed fall back to their name.

{
  major:      "",
  minor:      "m",
  diminished: "\u00B0",
  augmented:  "+"
}.freeze
ACCIDENTAL_DISPLAY =

Semitone offset => the accidental that writes it.

{ -1 => "b", 0 => "", 1 => "#" }.freeze
ACCIDENTAL_OFFSETS =

Inverse of ACCIDENTAL_DISPLAY, for parsing.

ACCIDENTAL_DISPLAY.invert.freeze

Class Method Summary collapse

Class Method Details

.format(degree:, quality: :major, accidental_offset: 0) ⇒ Object

Formats a degree + quality as an arabic Nashville number, parseable back by .parse: (2, :minor) => "2m", (7, :diminished) => "7°".



96
97
98
99
# File 'lib/outro_rails/theory/nashville_number.rb', line 96

def self.format(degree:, quality: :major, accidental_offset: 0)
  "#{ACCIDENTAL_DISPLAY.fetch(accidental_offset)}#{degree}" \
    "#{QUALITY_DISPLAY.fetch(quality) { quality.to_s }}"
end

.parse(input) ⇒ Object

Parses either notation into a ParseResult, trying arabic first. Raises ArgumentError on anything unrecognized.

Raises:

  • (ArgumentError)


73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/outro_rails/theory/nashville_number.rb', line 73

def self.parse(input)
  text = input.to_s.strip
  raise ArgumentError, "blank Nashville number" if text.empty?

  if (match = text.match(ARABIC_PATTERN))
    from_match(match,
               degree: Integer(match[:degree]),
               explicit_quality: nil)
  elsif (match = text.match(ROMAN_PATTERN))
    numeral = match[:numeral]
    degree = ROMAN_DEGREES.fetch(numeral.downcase) do
      raise ArgumentError, "unknown roman numeral #{numeral.inspect}"
    end
    # Case carries quality: IV is major, iv is minor.
    explicit_quality = numeral == numeral.downcase ? :minor : :major
    from_match(match, degree: degree, explicit_quality: explicit_quality)
  else
    raise ArgumentError, "cannot parse Nashville number #{input.inspect}"
  end
end

.roman(degree:, quality: :major, accidental_offset: 0) ⇒ Object

The same chord as a roman numeral, case carrying the quality: (2, :minor) => "ii", (5, :major) => "V", (7, :diminished) => "vii°".



103
104
105
106
107
108
109
110
111
112
113
# File 'lib/outro_rails/theory/nashville_number.rb', line 103

def self.roman(degree:, quality: :major, accidental_offset: 0)
  numeral = ROMAN_NUMERALS.fetch(degree - 1)
  accidental = ACCIDENTAL_DISPLAY.fetch(accidental_offset)

  case quality
  when :minor      then "#{accidental}#{numeral.downcase}"
  when :diminished then "#{accidental}#{numeral.downcase}°"
  when :augmented  then "#{accidental}#{numeral}+"
  else                  "#{accidental}#{numeral}"
  end
end