# 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