musicality 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/ChangeLog.md +8 -1
  3. data/bin/midify +3 -4
  4. data/examples/composition/auto_counterpoint.rb +53 -0
  5. data/examples/composition/part_generator.rb +51 -0
  6. data/examples/composition/scale_exercise.rb +41 -0
  7. data/examples/{hip.rb → notation/hip.rb} +1 -1
  8. data/examples/{missed_connection.rb → notation/missed_connection.rb} +1 -1
  9. data/examples/{song1.rb → notation/song1.rb} +1 -1
  10. data/examples/{song2.rb → notation/song2.rb} +1 -1
  11. data/lib/musicality.rb +34 -4
  12. data/lib/musicality/composition/generation/counterpoint_generator.rb +153 -0
  13. data/lib/musicality/composition/generation/random_rhythm_generator.rb +39 -0
  14. data/lib/musicality/composition/model/pitch_class.rb +33 -0
  15. data/lib/musicality/composition/model/pitch_classes.rb +22 -0
  16. data/lib/musicality/composition/model/scale.rb +34 -0
  17. data/lib/musicality/composition/model/scale_class.rb +37 -0
  18. data/lib/musicality/composition/model/scale_classes.rb +91 -0
  19. data/lib/musicality/composition/note_generation.rb +31 -0
  20. data/lib/musicality/composition/transposition.rb +8 -0
  21. data/lib/musicality/composition/util/adding_sequence.rb +24 -0
  22. data/lib/musicality/composition/util/biinfinite_sequence.rb +130 -0
  23. data/lib/musicality/composition/util/compound_sequence.rb +44 -0
  24. data/lib/musicality/composition/util/probabilities.rb +20 -0
  25. data/lib/musicality/composition/util/random_sampler.rb +26 -0
  26. data/lib/musicality/composition/util/repeating_sequence.rb +24 -0
  27. data/lib/musicality/errors.rb +2 -0
  28. data/lib/musicality/notation/conversion/score_conversion.rb +1 -1
  29. data/lib/musicality/notation/conversion/score_converter.rb +3 -3
  30. data/lib/musicality/notation/model/link.rb +26 -24
  31. data/lib/musicality/notation/model/links.rb +11 -0
  32. data/lib/musicality/notation/model/note.rb +14 -15
  33. data/lib/musicality/notation/model/part.rb +3 -3
  34. data/lib/musicality/notation/model/pitch.rb +8 -0
  35. data/lib/musicality/notation/model/score.rb +70 -44
  36. data/lib/musicality/notation/model/symbols.rb +22 -0
  37. data/lib/musicality/notation/packing/score_packing.rb +2 -3
  38. data/lib/musicality/notation/parsing/articulation_parsing.rb +4 -4
  39. data/lib/musicality/notation/parsing/articulation_parsing.treetop +2 -2
  40. data/lib/musicality/notation/parsing/link_nodes.rb +2 -14
  41. data/lib/musicality/notation/parsing/link_parsing.rb +9 -107
  42. data/lib/musicality/notation/parsing/link_parsing.treetop +4 -12
  43. data/lib/musicality/notation/parsing/note_node.rb +23 -21
  44. data/lib/musicality/notation/parsing/note_parsing.rb +70 -70
  45. data/lib/musicality/notation/parsing/note_parsing.treetop +6 -3
  46. data/lib/musicality/notation/parsing/pitch_node.rb +4 -2
  47. data/lib/musicality/performance/conversion/score_collator.rb +3 -3
  48. data/lib/musicality/performance/midi/midi_util.rb +13 -6
  49. data/lib/musicality/performance/midi/score_sequencing.rb +17 -0
  50. data/lib/musicality/printing/lilypond/errors.rb +5 -0
  51. data/lib/musicality/printing/lilypond/meter_engraving.rb +11 -0
  52. data/lib/musicality/printing/lilypond/note_engraving.rb +53 -0
  53. data/lib/musicality/printing/lilypond/part_engraver.rb +12 -0
  54. data/lib/musicality/printing/lilypond/pitch_engraving.rb +30 -0
  55. data/lib/musicality/printing/lilypond/score_engraver.rb +78 -0
  56. data/lib/musicality/version.rb +1 -1
  57. data/spec/composition/generation/random_rhythm_generator_spec.rb +50 -0
  58. data/spec/composition/model/pitch_class_spec.rb +75 -0
  59. data/spec/composition/model/pitch_classes_spec.rb +24 -0
  60. data/spec/composition/model/scale_class_spec.rb +98 -0
  61. data/spec/composition/model/scale_spec.rb +110 -0
  62. data/spec/composition/note_generation_spec.rb +113 -0
  63. data/spec/composition/transposition_spec.rb +17 -0
  64. data/spec/composition/util/adding_sequence_spec.rb +176 -0
  65. data/spec/composition/util/compound_sequence_spec.rb +50 -0
  66. data/spec/composition/util/probabilities_spec.rb +39 -0
  67. data/spec/composition/util/random_sampler_spec.rb +47 -0
  68. data/spec/composition/util/repeating_sequence_spec.rb +151 -0
  69. data/spec/notation/conversion/score_conversion_spec.rb +3 -3
  70. data/spec/notation/conversion/score_converter_spec.rb +24 -24
  71. data/spec/notation/model/link_spec.rb +27 -25
  72. data/spec/notation/model/note_spec.rb +9 -6
  73. data/spec/notation/model/pitch_spec.rb +24 -1
  74. data/spec/notation/model/score_spec.rb +57 -16
  75. data/spec/notation/packing/score_packing_spec.rb +134 -206
  76. data/spec/notation/parsing/articulation_parsing_spec.rb +1 -8
  77. data/spec/notation/parsing/convenience_methods_spec.rb +1 -1
  78. data/spec/notation/parsing/link_nodes_spec.rb +3 -4
  79. data/spec/notation/parsing/link_parsing_spec.rb +10 -4
  80. data/spec/notation/parsing/note_node_spec.rb +8 -7
  81. data/spec/notation/parsing/note_parsing_spec.rb +9 -12
  82. data/spec/performance/conversion/score_collator_spec.rb +14 -14
  83. data/spec/performance/midi/midi_util_spec.rb +26 -0
  84. data/spec/performance/midi/score_sequencer_spec.rb +1 -1
  85. metadata +57 -12
  86. data/lib/musicality/notation/model/program.rb +0 -53
  87. data/lib/musicality/notation/packing/program_packing.rb +0 -16
  88. data/spec/notation/model/program_spec.rb +0 -50
  89. data/spec/notation/packing/program_packing_spec.rb +0 -33
@@ -0,0 +1,44 @@
1
+ module Musicality
2
+
3
+ class CompoundSequence
4
+ def initialize combine_method, sequences
5
+ @seqs = sequences
6
+ @comb_meth = combine_method
7
+ end
8
+
9
+ def at offset
10
+ if offset.is_a? Enumerable
11
+ return enum_for(:at,offset) unless block_given?
12
+ enums = @seqs.map {|s| s.at(offset) }
13
+ offset.size.times { yield combine(enums.map {|e| e.next }) }
14
+ else
15
+ vals = @seqs.map {|s| s.at(offset) }
16
+ return combine(vals)
17
+ end
18
+ end
19
+
20
+ def take n
21
+ return enum_for(:take,n) unless block_given?
22
+ enums = @seqs.map {|s| s.take(n) }
23
+ n.times { yield combine(enums.map {|e| e.next }) }
24
+ end
25
+
26
+ def over range
27
+ return enum_for(:over,range) unless block_given?
28
+ enums = @seqs.map {|s| s.over(range) }
29
+ range.size.times { yield combine(enums.map {|e| e.next }) }
30
+ end
31
+
32
+ def take_back n
33
+ return enum_for(:take_back,n) unless block_given?
34
+ enums = @seqs.map {|s| s.take_back(n) }
35
+ n.times { yield combine(enums.map {|e| e.next }) }
36
+ end
37
+
38
+ private
39
+ def combine vals
40
+ vals[1..-1].inject(vals.first,@comb_meth)
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,20 @@
1
+ module Musicality
2
+
3
+ class Probabilities
4
+ def self.uniform n
5
+ probs = [1/n.to_f]*n
6
+ sum = probs.inject(0,:+)
7
+ if sum != 1
8
+ probs[0] += (1 - sum)
9
+ end
10
+ return probs
11
+ end
12
+
13
+ def self.random n
14
+ cumulative_probs = Array.new(n-1){ rand }.sort + [1]
15
+ x0 = 0
16
+ cumulative_probs.map {|x| y = x - x0; x0 = x; y }
17
+ end
18
+ end
19
+
20
+ end
@@ -0,0 +1,26 @@
1
+ module Musicality
2
+
3
+ class RandomSampler
4
+ attr_reader :values, :probabilities
5
+ def initialize vals, probs
6
+ @values, @probabilities = vals, probs
7
+ total_prob = probs.inject(0,:+)
8
+ raise ArgumentError, "Total probability is not 1" if total_prob != 1
9
+
10
+ offsets = AddingSequence.new(probs).over(1...probs.size).to_a
11
+ changes = vals[1..-1].map{|val| Change::Immediate.new(val) }
12
+
13
+ value_changes = Hash[ [offsets, changes].transpose ]
14
+ @val_comp = ValueComputer.new(vals.first, value_changes)
15
+ end
16
+
17
+ def sample n=nil
18
+ if n.nil?
19
+ return @val_comp.at(rand)
20
+ else
21
+ return Array.new(n){ @val_comp.at(rand) }
22
+ end
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,24 @@
1
+ module Musicality
2
+
3
+ class RepeatingSequence
4
+ include BiInfiniteSequence
5
+
6
+ attr_reader :start_value
7
+ def initialize pattern
8
+ raise EmptyError if pattern.empty?
9
+ @pattern = pattern
10
+ @n = pattern.size
11
+ @start_value = pattern.first
12
+ end
13
+
14
+ def pattern_size; @pattern.size; end
15
+
16
+ def next_value cur_val, cur_idx
17
+ @pattern[(cur_idx + 1) % @n]
18
+ end
19
+ def prev_value cur_val, cur_idx
20
+ @pattern[(cur_idx - 1) % @n]
21
+ end
22
+ end
23
+
24
+ end
@@ -5,6 +5,8 @@ module Musicality
5
5
  class NonIntegerError < StandardError; end
6
6
  class NonRationalError < StandardError; end
7
7
  class NonIncreasingError < StandardError; end
8
+ class DecreasingError < StandardError; end
8
9
  class NotValidError < StandardError; end
9
10
  class DomainError < StandardError; end
11
+ class EmptyError < StandardError; end
10
12
  end
@@ -27,7 +27,7 @@ class Score
27
27
  @parts.values.each do |part|
28
28
  part.dynamic_changes.each {|moff,change| moffs += change.offsets(moff) }
29
29
  end
30
- moffs += @program.segments.map {|seg| [seg.first, seg.last] }.flatten
30
+ moffs += @program.map {|seg| [seg.first, seg.last] }.flatten
31
31
  return moffs.sort
32
32
  end
33
33
 
@@ -25,9 +25,9 @@ class ScoreConverter
25
25
  end
26
26
 
27
27
  def self.convert_program program, offset_map
28
- Program.new(program.segments.map do |segment|
28
+ program.map do |segment|
29
29
  offset_map[segment.first]...offset_map[segment.last]
30
- end)
30
+ end
31
31
  end
32
32
 
33
33
  class TempoBased
@@ -44,7 +44,7 @@ class ScoreConverter
44
44
  part.notes.each {|note| noffs.add(noff += note.duration) }
45
45
  part.dynamic_changes.each {|noff2,change| noffs += change.offsets(noff2) }
46
46
  end
47
- noffs += @program.segments.map {|seg| [seg.first, seg.last] }.flatten
47
+ noffs += @program.map {|seg| [seg.first, seg.last] }.flatten
48
48
  return noffs.sort
49
49
  end
50
50
 
@@ -10,24 +10,26 @@ class Link
10
10
  Marshal.load(Marshal.dump(self))
11
11
  end
12
12
 
13
- class Tie < Link
14
- def initialize; end
15
-
16
- def ==(other)
17
- self.class == other.class
18
- end
19
-
20
- def transpose diff
21
- self.clone.transpose! diff
22
- end
23
-
24
- def transpose! diff
25
- return self
26
- end
27
-
28
- def to_s; "="; end
13
+ def transpose diff
14
+ self.clone.transpose! diff
29
15
  end
30
16
 
17
+ def transpose! diff
18
+ return self
19
+ end
20
+
21
+ def ==(other)
22
+ self.class == other.class
23
+ end
24
+
25
+ def to_s
26
+ self.class::LINK_CHAR
27
+ end
28
+
29
+ class Tie < Link
30
+ LINK_CHAR = LINK_SYMBOLS[Links::TIE]
31
+ end
32
+
31
33
  class TargetedLink < Link
32
34
  attr_accessor :target_pitch
33
35
 
@@ -36,7 +38,7 @@ class Link
36
38
  end
37
39
 
38
40
  def ==(other)
39
- self.class == other.class && @target_pitch == other.target_pitch
41
+ super && @target_pitch == other.target_pitch
40
42
  end
41
43
 
42
44
  def transpose diff
@@ -49,24 +51,24 @@ class Link
49
51
  end
50
52
 
51
53
  def to_s
52
- link_char + @target_pitch.to_s
54
+ super + @target_pitch.to_s
53
55
  end
54
56
  end
55
57
 
56
58
  class Glissando < TargetedLink
57
- def link_char; "~"; end
59
+ LINK_CHAR = LINK_SYMBOLS[Links::GLISSANDO]
58
60
  end
59
61
 
60
62
  class Portamento < TargetedLink
61
- def link_char; "/"; end
63
+ LINK_CHAR = LINK_SYMBOLS[Links::PORTAMENTO]
62
64
  end
63
-
65
+
64
66
  class Slur < TargetedLink
65
- def link_char; "="; end
67
+ LINK_CHAR = LINK_SYMBOLS[Links::SLUR]
66
68
  end
67
-
69
+
68
70
  class Legato < TargetedLink
69
- def link_char; "|"; end
71
+ LINK_CHAR = LINK_SYMBOLS[Links::LEGATO]
70
72
  end
71
73
  end
72
74
 
@@ -0,0 +1,11 @@
1
+ module Musicality
2
+
3
+ module Links
4
+ TIE = :tie
5
+ GLISSANDO = :glissando
6
+ PORTAMENTO = :portamento
7
+ SLUR = :slur
8
+ LEGATO = :legato
9
+ end
10
+
11
+ end
@@ -22,7 +22,7 @@ class Note
22
22
  end
23
23
 
24
24
  def check_methods
25
- [ :ensure_positive_duration ]
25
+ [ :ensure_positive_duration, :check_pitches ]
26
26
  end
27
27
 
28
28
  def ensure_positive_duration
@@ -31,6 +31,13 @@ class Note
31
31
  end
32
32
  end
33
33
 
34
+ def check_pitches
35
+ non_pitches = @pitches.select {|p| !p.is_a?(Pitch) }
36
+ if non_pitches.any?
37
+ raise TypeError, "Found non-pitches: #{non_pitches}"
38
+ end
39
+ end
40
+
34
41
  def == other
35
42
  return (@duration == other.duration) &&
36
43
  (self.pitches == other.pitches) &&
@@ -73,17 +80,7 @@ class Note
73
80
  else
74
81
  dur_str = d.to_s
75
82
  end
76
-
77
- art_str = case @articulation
78
- when Articulations::SLUR then "="
79
- when Articulations::LEGATO then "|"
80
- when Articulations::TENUTO then "_"
81
- when Articulations::PORTATO then "%"
82
- when Articulations::STACCATO then "."
83
- when Articulations::STACCATISSIMO then "'"
84
- else ""
85
- end
86
-
83
+
87
84
  pitch_links_str = @pitches.map do |p|
88
85
  if @links.has_key?(p)
89
86
  p.to_s + @links[p].to_s
@@ -91,9 +88,11 @@ class Note
91
88
  p.to_s
92
89
  end
93
90
  end.join(",")
94
-
95
- acc_str = @accented ? "!" : ""
96
- return dur_str + art_str + pitch_links_str + acc_str
91
+
92
+ art_str = ARTICULATION_SYMBOLS[@articulation] || ""
93
+ acc_str = @accented ? ACCENT_SYMBOL : ""
94
+
95
+ return dur_str + pitch_links_str + art_str + acc_str
97
96
  end
98
97
 
99
98
  def self.add_note_method(name, dur)
@@ -16,7 +16,7 @@ class Part
16
16
  end
17
17
 
18
18
  def check_methods
19
- [:ensure_start_dynamic, :ensure_dynamic_change_values_range ]
19
+ [:check_start_dynamic, :check_dynamic_changes]
20
20
  end
21
21
 
22
22
  def validatables
@@ -37,13 +37,13 @@ class Part
37
37
  return @notes.inject(0) { |sum, note| sum + note.duration }
38
38
  end
39
39
 
40
- def ensure_start_dynamic
40
+ def check_start_dynamic
41
41
  unless @start_dynamic.between?(0,1)
42
42
  raise RangeError, "start dynamic #{@start_dynamic} is not between 0 and 1"
43
43
  end
44
44
  end
45
45
 
46
- def ensure_dynamic_change_values_range
46
+ def check_dynamic_changes
47
47
  outofrange = @dynamic_changes.values.select {|v| !v.end_value.between?(0,1) }
48
48
  if outofrange.any?
49
49
  raise RangeError, "dynamic change values #{outofrange} are not between 0 and 1"
@@ -96,6 +96,14 @@ class Pitch
96
96
  Pitch.new(cent: (@total_cents + semitones * CENTS_PER_SEMITONE).round)
97
97
  end
98
98
 
99
+ def + semitones
100
+ transpose(semitones)
101
+ end
102
+
103
+ def - semitones
104
+ transpose(-semitones)
105
+ end
106
+
99
107
  def total_semitones
100
108
  Rational(@total_cents, CENTS_PER_SEMITONE)
101
109
  end
@@ -4,15 +4,14 @@ class Score
4
4
  include Validatable
5
5
  attr_accessor :parts, :program
6
6
 
7
- def initialize parts: {}, program: Program.new
7
+ def initialize parts: {}, program: []
8
8
  @parts = parts
9
9
  @program = program
10
10
  yield(self) if block_given?
11
11
  end
12
12
 
13
- def validatables
14
- [ @program ] + @parts.values
15
- end
13
+ def validatables; @parts.values; end
14
+ def check_methods; [:check_program, :check_parts ]; end
16
15
 
17
16
  def clone
18
17
  Marshal.load(Marshal.dump(self))
@@ -27,7 +26,7 @@ class Score
27
26
  end
28
27
 
29
28
  def collated?
30
- @program.segments.size == 1 && @program.segments[0].first == 0
29
+ @program.size == 1 && @program[0].first == 0
31
30
  end
32
31
 
33
32
  class Timed < Score
@@ -39,16 +38,14 @@ class Score
39
38
  class TempoBased < Score
40
39
  attr_accessor :start_tempo, :tempo_changes
41
40
 
42
- def initialize start_tempo, tempo_changes: {}, parts: {}, program: Program.new
41
+ def initialize start_tempo, tempo_changes: {}, parts: {}, program: []
43
42
  @start_tempo = start_tempo
44
43
  @tempo_changes = tempo_changes
45
44
  super(parts: parts, program: program)
46
-
47
- yield(self) if block_given?
48
45
  end
49
46
 
50
47
  def check_methods
51
- [:check_start_tempo, :check_tempo_changes]
48
+ super() + [:check_start_tempo, :check_tempo_changes]
52
49
  end
53
50
 
54
51
  def ==(other)
@@ -60,16 +57,23 @@ class Score
60
57
  self.duration
61
58
  end
62
59
 
60
+ private
61
+
63
62
  def check_start_tempo
64
63
  if @start_tempo <= 0
65
- raise NonPositiveError, "start tempo (#{@start_tempo}) is not positive"
64
+ raise NonPositiveError, "Start tempo (#{@start_tempo}) is not positive"
66
65
  end
67
66
  end
68
67
 
69
68
  def check_tempo_changes
69
+ badtypes = @tempo_changes.select {|k,v| !v.end_value.is_a?(Numeric) }
70
+ if badtypes.any?
71
+ raise TypeError, "Found non-numeric tempo change values: #{badtypes}"
72
+ end
73
+
70
74
  badvalues = @tempo_changes.select {|k,v| v.end_value <= 0 }
71
75
  if badvalues.any?
72
- raise NonPositiveError, "tempo changes (#{badvalues}) are not positive"
76
+ raise NonPositiveError, "Found non-positive tempo changes values: #{badvalues}"
73
77
  end
74
78
  end
75
79
  end
@@ -84,58 +88,29 @@ class Score
84
88
  class Measured < Score::TempoBased
85
89
  attr_accessor :start_meter, :meter_changes
86
90
 
87
- def initialize start_meter, start_tempo, meter_changes: {}, tempo_changes: {}, parts: {}, program: Program.new
91
+ def initialize start_meter, start_tempo, meter_changes: {}, tempo_changes: {}, parts: {}, program: []
88
92
  @start_meter = start_meter
89
93
  @meter_changes = meter_changes
90
94
 
91
95
  super(start_tempo, tempo_changes: tempo_changes,
92
96
  program: program, parts: parts)
93
- yield(self) if block_given?
94
97
  end
95
98
 
96
99
  def check_methods
97
- super() + [:check_startmeter_type, :check_meterchange_types,
98
- :check_meterchange_durs, :check_meterchange_offsets]
100
+ super() + [:check_start_meter, :check_meter_changes]
99
101
  end
100
102
 
101
103
  def validatables
102
104
  super() + [ @start_meter ] + @meter_changes.values.map {|v| v.end_value}
103
105
  end
104
106
 
105
- def check_startmeter_type
106
- unless @start_meter.is_a? Meter
107
- raise TypeError, "start meter #{@start_meter} is not a Meter object"
108
- end
109
- end
110
-
111
- def check_meterchange_types
112
- badtypes = @meter_changes.select {|k,v| !v.end_value.is_a?(Meter) }
113
- if badtypes.any?
114
- raise TypeError, "meter change values #{nonmeter_values} are not Meter objects"
115
- end
116
- end
117
-
118
- def check_meterchange_offsets
119
- badoffsets = @meter_changes.select {|k,v| k != k.to_i }
120
- if badoffsets.any?
121
- raise NonIntegerError, "meter changes #{badoffsets} have non-integer offsets"
122
- end
123
- end
124
-
125
- def check_meterchange_durs
126
- nonzero_duration = @meter_changes.select {|k,v| !v.is_a?(Change::Immediate) }
127
- if nonzero_duration.any?
128
- raise NonZeroError, "meter changes #{nonzero_duration} are not immediate"
129
- end
130
- end
131
-
132
107
  def ==(other)
133
108
  return super(other) && @start_meter == other.start_meter &&
134
109
  @meter_changes == other.meter_changes
135
110
  end
136
111
 
137
- def measures_long
138
- noff_end = self.notes_long
112
+ def measures_long note_dur = self.notes_long
113
+ noff_end = note_dur
139
114
  noff_prev = 0.to_r
140
115
  moff_prev, mdur_prev = 0.to_r, @start_meter.measure_duration
141
116
 
@@ -154,6 +129,57 @@ class Score
154
129
  end
155
130
  return moff_prev + Rational(noff_end - noff_prev, mdur_prev)
156
131
  end
132
+
133
+ private
134
+
135
+ def check_start_meter
136
+ unless @start_meter.is_a? Meter
137
+ raise TypeError, "Start meter #{@start_meter} is not a Meter object"
138
+ end
139
+ end
140
+
141
+ def check_meter_changes
142
+ badtypes = @meter_changes.select {|k,v| !v.end_value.is_a?(Meter) }
143
+ if badtypes.any?
144
+ raise TypeError, "Found meter change values that are not Meter objects: #{nonmeter_values}"
145
+ end
146
+
147
+ badoffsets = @meter_changes.select {|k,v| k != k.to_i }
148
+ if badoffsets.any?
149
+ raise NonIntegerError, "Found meter changes at non-integer offsets: #{badoffsets}"
150
+ end
151
+
152
+ nonzero_duration = @meter_changes.select {|k,v| !v.is_a?(Change::Immediate) }
153
+ if nonzero_duration.any?
154
+ raise NonZeroError, "Found meter changes that are not immediate: #{nonzero_duration}"
155
+ end
156
+ end
157
+ end
158
+
159
+ private
160
+
161
+ def check_program
162
+ non_ranges = @program.select {|x| !x.is_a?(Range) }
163
+ if non_ranges.any?
164
+ raise TypeError, "Non-Range program element(s) found: #{non_ranges}"
165
+ end
166
+
167
+ non_increasing = @program.select {|seg| seg.first >= seg.last }
168
+ if non_increasing.any?
169
+ raise NonIncreasingError, "Non-increasing program range(s) found: #{non_increasing}"
170
+ end
171
+
172
+ negative = @program.select {|seg| seg.first < 0 || seg.last < 0 }
173
+ if negative.any?
174
+ raise NegativeError, "Program range(s) with negative value(s) found: #{negative}"
175
+ end
176
+ end
177
+
178
+ def check_parts
179
+ non_parts = @parts.values.select {|x| !x.is_a?(Part) }
180
+ if non_parts.any?
181
+ raise TypeError, "Non-Part part value(s) found: #{non_parts}"
182
+ end
157
183
  end
158
184
  end
159
185