Class: HeadMusic::Style::Guides::Base
- Inherits:
-
Object
- Object
- HeadMusic::Style::Guides::Base
- Defined in:
- lib/head_music/style/guides/base.rb
Overview
Base class for style guides: declares guidelines in three tiers and assesses a voice against them.
Tier is the list an item is declared in rather than a property of the item, because the cores are shared objects – ContourMelody treats as background exactly what DiatonicMelody teaches, and one frozen item cannot carry both.
Direct Known Subclasses
Constant Summary collapse
- TIERS =
%i[gate primary secondary].freeze
Class Method Summary collapse
-
.assess(voice) ⇒ Object
-
.assess_items(voice) ⇒ Object
The material GuideAssessment grades, stopping at a failed gate.
-
.categories ⇒ Object
The guide-side twin of GuideAssessment#assessments: one category, so a registry sweep can ask every entry the same question.
-
.category ⇒ Object
An open enum: :melody or :harmony today, declared on the marker bases.
-
.composite? ⇒ Boolean
-
.declarations ⇒ Object
private
Never inherited: a subclass that omits a list gets an empty one.
-
.display_name ⇒ Object
-
.gate_items(*entries, except: nil) ⇒ Object
Preconditions: is this voice assessable at all?.
-
.guide_items ⇒ Object
-
.instruction ⇒ Object
-
.items_by_tier ⇒ Object
A guide whose lists depend on configuration overrides this with a keyword signature, so an unconfigured use raises rather than grading a voice against nothing at a plausible 1.0.
-
.key ⇒ Object
-
.normalize(tiers) ⇒ Object
protected
-
.primary_items(*entries, except: nil) ⇒ Object
What this guide is about.
-
.reject_duplicates(resolved) ⇒ Object
private
MinimumNotes as a gate and again as a stylistic minimum asks two questions; the same configuration in two tiers is double-counting.
-
.secondary_items(*entries, except: nil) ⇒ Object
Background craft this guide inherits rather than teaches.
-
.tier_items(tier, entries, except) ⇒ Object
private
-
.with(**options) ⇒ Object
Rejected here rather than at the first assessment, where Ruby’s bare “wrong number of arguments” would name neither the guide nor the option.
-
.wrap_list(entries, excluded = nil) ⇒ Object
private
except: applies to the entries of the call carrying it, not the tier: DiatonicMelody drops a core item and then declares its own configured one.
Class Method Details
.assess(voice) ⇒ Object
34 35 36 |
# File 'lib/head_music/style/guides/base.rb', line 34 def assess(voice) HeadMusic::Style::GuideAssessment.new(self, voice) end |
.assess_items(voice) ⇒ Object
The material GuideAssessment grades, stopping at a failed gate.
39 40 41 |
# File 'lib/head_music/style/guides/base.rb', line 39 def assess_items(voice) HeadMusic::Style::Guides::Assessment.assess_items(voice, items_by_tier) end |
.categories ⇒ Object
The guide-side twin of GuideAssessment#assessments: one category, so a registry sweep can ask every entry the same question.
68 69 70 |
# File 'lib/head_music/style/guides/base.rb', line 68 def categories [category].compact end |
.category ⇒ Object
An open enum: :melody or :harmony today, declared on the marker bases.
58 59 60 |
# File 'lib/head_music/style/guides/base.rb', line 58 def category nil end |
.composite? ⇒ Boolean
62 63 64 |
# File 'lib/head_music/style/guides/base.rb', line 62 def composite? false end |
.declarations ⇒ Object (private)
Never inherited: a subclass that omits a list gets an empty one.
121 122 123 |
# File 'lib/head_music/style/guides/base.rb', line 121 def declarations @declarations ||= {} end |
.display_name ⇒ Object
72 73 74 |
# File 'lib/head_music/style/guides/base.rb', line 72 def display_name HeadMusic::Style::Guide.display_name_for(key) end |
.gate_items(*entries, except: nil) ⇒ Object
Preconditions: is this voice assessable at all?
15 |
# File 'lib/head_music/style/guides/base.rb', line 15 def gate_items(*entries, except: nil) = tier_items(:gate, entries, except) |
.guide_items ⇒ Object
23 24 25 |
# File 'lib/head_music/style/guides/base.rb', line 23 def guide_items @guide_items ||= TIERS.flat_map { |tier| items_by_tier[tier] }.freeze end |
.instruction ⇒ Object
76 77 78 |
# File 'lib/head_music/style/guides/base.rb', line 76 def instruction HeadMusic::Style::Guide.instruction_for(key) end |
.items_by_tier ⇒ Object
A guide whose lists depend on configuration overrides this with a keyword signature, so an unconfigured use raises rather than grading a voice against nothing at a plausible 1.0.
30 31 32 |
# File 'lib/head_music/style/guides/base.rb', line 30 def items_by_tier @items_by_tier ||= normalize(declarations) end |
.key ⇒ Object
53 54 55 |
# File 'lib/head_music/style/guides/base.rb', line 53 def key HeadMusic::Utilities::Case.to_snake_case(name.split("::").last) end |
.normalize(tiers) ⇒ Object (protected)
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/head_music/style/guides/base.rb', line 82 def normalize(tiers) resolved = TIERS.to_h { |tier| [tier, wrap_list(tiers[tier])] }.freeze # ArgumentError, not NotImplementedError: the latter is a ScriptError and # so escapes an ordinary rescue. if resolved.values.all?(&:empty?) raise ArgumentError, "#{name} declares no guide items" end # A guide that is all background has no subject, and grading it 1.0 in # silence is the "nothing to find fault in" confusion. # # A gate-only guide is caught by the same check, deliberately and with no # exemption: a guide that only decides whether a voice is assessable has # nothing to grade it against, and an exemption would leave two spellings # of "this guide teaches nothing" -- one that raises and one that quietly # returns 1.0. # # The declared items are named because the class name is not enough: the # six contour guides are one class configured six ways. if resolved[:primary].empty? raise ArgumentError, "#{name} declares no primary guide items, so it teaches nothing: " \ "#{resolved.reject { |_tier, items| items.empty? }.transform_values { |items| items.map(&:inspect) }.inspect}" end reject_duplicates(resolved) resolved end |
.primary_items(*entries, except: nil) ⇒ Object
What this guide is about.
18 |
# File 'lib/head_music/style/guides/base.rb', line 18 def primary_items(*entries, except: nil) = tier_items(:primary, entries, except) |
.reject_duplicates(resolved) ⇒ Object (private)
MinimumNotes as a gate and again as a stylistic minimum asks two questions; the same configuration in two tiers is double-counting.
136 137 138 139 140 141 142 143 |
# File 'lib/head_music/style/guides/base.rb', line 136 def reject_duplicates(resolved) duplicated = resolved.values.flatten.tally.select { |_item, count| count > 1 }.keys return if duplicated.empty? raise ArgumentError, "#{name} declares the same guideline and configuration in more than one tier: " \ "#{duplicated.map(&:inspect).join(", ")}" end |
.secondary_items(*entries, except: nil) ⇒ Object
Background craft this guide inherits rather than teaches.
21 |
# File 'lib/head_music/style/guides/base.rb', line 21 def secondary_items(*entries, except: nil) = tier_items(:secondary, entries, except) |
.tier_items(tier, entries, except) ⇒ Object (private)
113 114 115 116 117 118 |
# File 'lib/head_music/style/guides/base.rb', line 113 def tier_items(tier, entries, except) return items_by_tier[tier] if entries.empty? && except.nil? declarations[tier] = [*declarations[tier], *wrap_list(entries, except)] nil end |
.with(**options) ⇒ Object
Rejected here rather than at the first assessment, where Ruby’s bare “wrong number of arguments” would name neither the guide nor the option.
45 46 47 48 49 50 51 |
# File 'lib/head_music/style/guides/base.rb', line 45 def with(**) if .any? && method(:items_by_tier).parameters.empty? raise ArgumentError, "#{name} takes no configuration, so it cannot be given: #{.keys.join(", ")}" end HeadMusic::Style::Guides::Configured.new(self, ) end |
.wrap_list(entries, excluded = nil) ⇒ Object (private)
except: applies to the entries of the call carrying it, not the tier: DiatonicMelody drops a core item and then declares its own configured one.
127 128 129 130 131 132 |
# File 'lib/head_music/style/guides/base.rb', line 127 def wrap_list(entries, excluded = nil) items = Array(entries).compact.map { |entry| HeadMusic::Style::GuideItem.wrap(entry) } return items.freeze if excluded.nil? items.reject { |item| Array(excluded).include?(item.guideline) }.freeze end |