musicality 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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