lib/coltrane/theory/interval_class.rb
# frozen_string_literal: true
module Coltrane
module Theory
# Interval class here is not related to the Object Oriented Programming context
# but to the fact that there is a class of intervals that can all be categorized
# as having the same quality.
#
# This class in specific still takes into account the order of intervals.
# C to D is a major second, but D to C is a minor seventh.
class IntervalClass < FrequencyInterval
QUALITY_SEQUENCE = [
%w[P],
%w[m M],
%w[m M],
%w[P A],
%w[P],
%w[m M],
%w[m M]
].freeze
ALTERATIONS = {
'A' => +1,
'd' => -1
}.freeze
SINGLE_DISTANCES_NAMES = %w[
Unison
Second
Third
Fourth
Fifth
Sixth
Seventh
].freeze
COMPOUND_DISTANCES_NAMES = [
'Octave',
'Ninth',
'Tenth',
'Eleventh',
'Twelfth',
'Thirteenth',
'Fourteenth',
'Double Octave'
].freeze
DISTANCES_NAMES = (SINGLE_DISTANCES_NAMES + COMPOUND_DISTANCES_NAMES).freeze
QUALITY_NAMES = {
'P' => 'Perfect',
'm' => 'Minor',
'M' => 'Major',
'A' => 'Augmented',
'd' => 'Diminished'
}.freeze
class << self
def distances_names
DISTANCES_NAMES
end
def distance_name(n)
DISTANCES_NAMES[n - 1]
end
def quality_name(q)
QUALITY_NAMES[q]
end
def names
@names ||= begin
SINGLE_DISTANCES_NAMES.each_with_index.reduce([]) do |i_names, (_d, i)|
i_names + QUALITY_SEQUENCE[i % 7].reduce([]) do |qs, q|
qs + ["#{q}#{i + 1}"]
end
end
end
end
def compound_names
@compound_names ||= all.map(&:compound_name)
end
def all_names_including_compound
@all_names_including_compound ||= names + compound_names
end
def full_names
@full_names ||= names.map { |n| expand_name(n) }
end
def all
@all ||= names.map { |n| IntervalClass[n] }
end
def full_names_including_compound
@full_names_including_compound ||=
all_names_including_compound.map { |n| expand_name(n) }
end
def split(interval)
interval.scan(/(\w)(\d\d?)/)[0]
end
def expand_name(name)
q, n = split(name)
(
case name
when /AA|dd/ then 'Double '
when /AAA|ddd/ then 'Triple '
else ''
end
) + "#{quality_name(q)} #{distance_name(n.to_i)}"
end
end
def initialize(arg)
super case arg
when FrequencyInterval then arg.semitones
when String
self.class.names.index(arg) ||
self.class.full_names.index(arg) ||
self.class.all_names_including_compound.index(arg) ||
self.class.full_names_including_compound.index(arg)
when Numeric then arg
else
raise WrongArgumentsError,
'Provide: [interval] || [name] || [number of semitones]'
end % 12 * 100
end
instance_eval { alias [] new }
def interval
Interval.new(letter_distance: distance, semitones: semitones)
end
def compound_interval
Interval.new(
letter_distance: distance,
semitones: semitones,
compound: true
)
end
alias compound compound_interval
def ==(other)
return false unless other.is_a? FrequencyInterval
(semitones % 12) == (other.semitones % 12)
end
def alteration
name.chars.reduce(0) { |a, s| a + (ALTERATIONS[s] || 0) }
end
def ascending
self.class[semitones.abs]
end
def descending
self.class[-semitones.abs]
end
def inversion
self.class[-semitones % 12]
end
def full_name
self.class.expand_name(name)
end
def name
self.class.names[semitones % 12]
end
def compound_name
"#{quality}#{distance + 7}"
end
def distance
self.class.split(name)[1].to_i
end
def quality
self.class.split(name)[0]
end
def +(other)
IntervalClass[semitones + other]
end
def -(other)
IntervalClass[semitones - other]
end
def -@
IntervalClass[-semitones]
end
private
def self.interval_by_full_name(arg)
NAMES.invert.each do |full_names, interval_name|
return INTERVALS.index(interval_name) if full_names.include?(arg)
end
raise IntervalNotFoundError, arg
end
end
end
end