phonology 0.0.1 → 0.0.5

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.
@@ -8,8 +8,9 @@ module Phonology
8
8
 
9
9
  # Pulmonic consonants
10
10
  PULMONIC = [:nasal, :plosive, :fricative, :approximant, :trill, :flap,
11
- :lateral, :bilabial, :labiodental, :dental, :alveolar, :postalveolar,
12
- :retroflex, :palatal, :velar, :uvular, :pharyngeal, :epiglottal, :glottal]
11
+ :lateral_fricative, :lateral_approximant, :lateral_flap, :bilabial,
12
+ :labiodental, :dental, :alveolar, :postalveolar, :retroflex, :palatal,
13
+ :velar, :uvular, :pharyngeal, :epiglottal, :glottal]
13
14
 
14
15
  # Non-pulmonic consonants
15
16
  NON_PULMONIC = [:click, :implosive, :ejective]
@@ -25,7 +26,8 @@ module Phonology
25
26
  ALL = ([:voiced] + PULMONIC + NON_PULMONIC + VOCALIC).to_set
26
27
 
27
28
  # Manner of articulation features
28
- MANNER = [:nasal, :plosive, :fricative, :approximant, :trill, :flap, :lateral] + NON_PULMONIC
29
+ MANNER = [:nasal, :plosive, :fricative, :approximant, :trill, :flap, :lateral_fricative,
30
+ :lateral_approximant, :lateral_flap] + NON_PULMONIC
29
31
 
30
32
  # Place of articulation features
31
33
  PLACE = CONSONANTAL.to_a - MANNER
@@ -42,122 +44,135 @@ module Phonology
42
44
  :coronal => [:dental, :alveolar, :postalveolar, :retroflex].to_set,
43
45
  :dorsal => [:palatal, :velar, :uvular].to_set,
44
46
  :radical => [:pharyngeal, :epiglottal].to_set,
45
- :vocalic => VOCALIC + [:rounded],
46
- :non_pulmonic => NON_PULMONIC,
47
- :pulmonic => PULMONIC,
47
+ :lateral => [:lateral_fricative, :lateral_approximant, :lateral_flap].to_set,
48
+ :liquid => [:flap, :lateral_fricative, :lateral_approximant, :lateral_flap].to_set,
49
+ :vocalic => (VOCALIC + [:rounded]).to_set,
50
+ :vocoid => (VOCALIC + [:approximant]).to_set,
51
+ :non_pulmonic => NON_PULMONIC.to_set,
52
+ :pulmonic => PULMONIC.to_set,
48
53
  :consonantal => CONSONANTAL
49
54
  }
50
55
 
56
+ # IPA diacritics for some sound modifiers such as devoicing, affricitization, etc.
57
+ DIACRITICS = {
58
+ :affricate => [0x0361]
59
+ }
60
+
51
61
  # Sets of distinctive features mapped to an IPA symbol
52
62
  SETS = {
53
- [:voiced, :bilabial, :nasal].to_set => [0x006d],
54
- [:voiced, :labiodental, :nasal].to_set => [0x0271],
55
- [:voiced, :alveolar, :nasal].to_set => [0x006e],
56
- [:voiced, :retroflex, :nasal].to_set => [0x0273],
57
- [:voiced, :palatal, :nasal].to_set => [0x0272],
58
- [:voiced, :velar, :nasal].to_set => [0x014b],
59
- [:voiced, :uvular, :nasal].to_set => [0x0274],
60
- [:bilabial, :plosive].to_set => [0x0070],
61
- [:voiced, :bilabial, :plosive].to_set => [0x0062],
62
- [:labiodental, :plosive].to_set => [0x0070, 0x032a],
63
- [:voiced, :labiodental, :plosive].to_set => [0x0062, 0x032a],
64
- [:alveolar, :plosive].to_set => [0x0074],
65
- [:voiced, :alveolar, :plosive].to_set => [0x0064],
66
- [:retroflex, :plosive].to_set => [0x0288],
67
- [:voiced, :retroflex, :plosive].to_set => [0x0256],
68
- [:palatal, :plosive].to_set => [0x0063],
69
- [:voiced, :palatal, :plosive].to_set => [0x025f],
70
- [:velar, :plosive].to_set => [0x006b],
71
- [:voiced, :velar, :plosive].to_set => [0x0261],
72
- [:uvular, :plosive].to_set => [0x0071],
73
- [:voiced, :uvular, :plosive].to_set => [0x0262],
74
- [:voiced, :epiglottal, :plosive].to_set => [0x02a1],
75
- [:voiced, :glottal, :plosive].to_set => [0x0294],
76
- [:bilabial, :fricative].to_set => [0x0278],
77
- [:voiced, :bilabial, :fricative].to_set => [0x03b2],
78
- [:labiodental, :fricative].to_set => [0x0066],
79
- [:voiced, :labiodental, :fricative].to_set => [0x0076],
80
- [:dental, :fricative].to_set => [0x03b8],
81
- [:voiced, :dental, :fricative].to_set => [0x00f0],
82
- [:alveolar, :fricative].to_set => [0x0073],
83
- [:voiced, :alveolar, :fricative].to_set => [0x007a],
84
- [:postalveolar, :fricative].to_set => [0x0283],
85
- [:voiced, :postalveolar, :fricative].to_set => [0x0292],
86
- [:retroflex, :fricative].to_set => [0x0282],
87
- [:voiced, :retroflex, :fricative].to_set => [0x0290],
88
- [:palatal, :fricative].to_set => [0x00e7],
89
- [:voiced, :palatal, :fricative].to_set => [0x029d],
90
- [:velar, :fricative].to_set => [0x0078],
91
- [:voiced, :velar, :fricative].to_set => [0x0263],
92
- [:uvular, :fricative].to_set => [0x03c7],
93
- [:voiced, :uvular, :fricative, :approximant].to_set => [0x0281],
94
- [:pharyngeal, :fricative].to_set => [0x0127],
95
- [:voiced, :pharyngeal, :fricative, :approximant].to_set => [0x0295],
96
- [:epiglottal, :fricative].to_set => [0x029c],
97
- [:voiced, :epiglottal, :fricative, :approximant].to_set => [0x02a2],
98
- [:glottal, :fricative, :approximant].to_set => [0x0068],
99
- [:voiced, :glottal, :fricative, :approximant].to_set => [0x0266],
100
- [:voiced, :bilabial, :approximant].to_set => [0x03b2, 0x031e],
101
- [:voiced, :labiodental, :approximant].to_set => [0x028b],
102
- [:voiced, :alveolar, :approximant].to_set => [0x0279],
103
- [:voiced, :retroflex, :approximant].to_set => [0x027b],
104
- [:voiced, :palatal, :approximant].to_set => [0x006a],
105
- [:voiced, :velar, :approximant].to_set => [0x0270],
106
- [:voiced, :bilabial, :trill].to_set => [0x0299],
107
- [:voiced, :alveolar, :trill].to_set => [0x0072],
108
- [:voiced, :uvular, :trill].to_set => [0x0280],
109
- [:voiced, :epiglottal, :trill].to_set => [0x044f],
110
- [:voiced, :bilabial, :flap].to_set => [0x2c71, 0x031f],
111
- [:voiced, :labiodental, :flap].to_set => [0x2c71],
112
- [:voiced, :alveolar, :flap].to_set => [0x027e],
113
- [:voiced, :retroflex, :flap].to_set => [0x027d],
114
- [:voiced, :uvular, :flap].to_set => [0x0262, 0x0306],
115
- [:voiced, :epiglottal, :flap].to_set => [0x02a1, 0x032f],
116
- [:alveolar, :lateral, :fricative].to_set => [0x026c],
117
- [:voiced, :alveolar, :lateral, :fricative].to_set => [0x026e],
118
- [:voiced, :alveolar, :lateral, :approximant].to_set => [0x006c],
119
- [:voiced, :retroflex, :lateral, :approximant].to_set => [0x026d],
120
- [:voiced, :palatal, :lateral, :approximant].to_set => [0x028e],
121
- [:voiced, :velar, :lateral, :approximant].to_set => [0x029f],
122
- [:voiced, :alveolar, :lateral, :flap].to_set => [0x027a],
123
- [:voiced, :palatal, :lateral, :flap].to_set => [0x028e, 0x032f],
124
- [:voiced, :velar, :lateral, :flap].to_set => [0x029f, 0x0306],
125
- [:voiced, :close, :front].to_set => [0x0069],
126
- [:voiced, :close, :front, :rounded].to_set => [0x0079],
127
- [:voiced, :close, :central].to_set => [0x0268],
128
- [:voiced, :close, :central, :rounded].to_set => [0x0289],
129
- [:voiced, :close, :back].to_set => [0x026f],
130
- [:voiced, :close, :back, :rounded].to_set => [0x0075],
131
- [:voiced, :near_close, :near_front].to_set => [0x026a],
132
- [:voiced, :near_close, :near_front, :rounded].to_set => [0x028f],
133
- [:voiced, :near_close, :near_back, :rounded].to_set => [0x028a],
134
- [:voiced, :close_mid, :front].to_set => [0x0065],
135
- [:voiced, :close_mid, :front, :rounded].to_set => [0x00f8],
136
- [:voiced, :close_mid, :central].to_set => [0x0258],
137
- [:voiced, :close_mid, :central, :rounded].to_set => [0x0275],
138
- [:voiced, :close_mid, :back].to_set => [0x0264],
139
- [:voiced, :close_mid, :back, :rounded].to_set => [0x006f],
140
- [:voiced, :mid, :central].to_set => [0x0259],
141
- [:voiced, :open_mid, :front].to_set => [0x025b],
142
- [:voiced, :open_mid, :front, :rounded].to_set => [0x0153],
143
- [:voiced, :open_mid, :central].to_set => [0x025c],
144
- [:voiced, :open_mid, :central, :rounded].to_set => [0x025e],
145
- [:voiced, :open_mid, :back].to_set => [0x028c],
146
- [:voiced, :open_mid, :back, :rounded].to_set => [0x0254],
147
- [:voiced, :near_open, :front].to_set => [0x00e6],
148
- [:voiced, :near_open, :central].to_set => [0x0250],
149
- [:voiced, :open, :front].to_set => [0x0061],
150
- [:voiced, :open, :front, :rounded].to_set => [0x0276],
151
- [:voiced, :open, :back].to_set => [0x0251],
152
- [:voiced, :open, :back, :rounded].to_set => [0x0252],
153
- # This char will appear in Unicode 6.0
154
- [:voiced, :retroflex, :lateral, :fricative].to_set => [0xa78e],
155
- # Not in UTF-8; used in SIL International's Charis/Doulos font's private
156
- # use area
157
- [:voiced, :palatal, :lateral, :fricative].to_set => [0xf267],
158
- [:voiced, :velar, :lateral, :fricative].to_set => [0xf268],
159
- [:voiced, :retroflex, :lateral, :flap].to_set => [0xf269]
63
+ [:voiced, :bilabial, :nasal].to_set => [0x006d],
64
+ [:voiced, :labiodental, :nasal].to_set => [0x0271],
65
+ [:voiced, :alveolar, :nasal].to_set => [0x006e],
66
+ [:voiced, :retroflex, :nasal].to_set => [0x0273],
67
+ [:voiced, :palatal, :nasal].to_set => [0x0272],
68
+ [:voiced, :velar, :nasal].to_set => [0x014b],
69
+ [:voiced, :uvular, :nasal].to_set => [0x0274],
70
+ [:bilabial, :plosive].to_set => [0x0070],
71
+ [:voiced, :bilabial, :plosive].to_set => [0x0062],
72
+ [:labiodental, :plosive].to_set => [0x0070, 0x032a],
73
+ [:voiced, :labiodental, :plosive].to_set => [0x0062, 0x032a],
74
+ [:alveolar, :plosive].to_set => [0x0074],
75
+ [:voiced, :alveolar, :plosive].to_set => [0x0064],
76
+ [:retroflex, :plosive].to_set => [0x0288],
77
+ [:voiced, :retroflex, :plosive].to_set => [0x0256],
78
+ [:palatal, :plosive].to_set => [0x0063],
79
+ [:voiced, :palatal, :plosive].to_set => [0x025f],
80
+ [:velar, :plosive].to_set => [0x006b],
81
+ [:voiced, :velar, :plosive].to_set => [0x0261],
82
+ [:uvular, :plosive].to_set => [0x0071],
83
+ [:voiced, :uvular, :plosive].to_set => [0x0262],
84
+ [:voiced, :epiglottal, :plosive].to_set => [0x02a1],
85
+ [:voiced, :glottal, :plosive].to_set => [0x0294],
86
+ [:bilabial, :fricative].to_set => [0x0278],
87
+ [:voiced, :bilabial, :fricative].to_set => [0x03b2],
88
+ [:labiodental, :fricative].to_set => [0x0066],
89
+ [:voiced, :labiodental, :fricative].to_set => [0x0076],
90
+ [:dental, :fricative].to_set => [0x03b8],
91
+ [:voiced, :dental, :fricative].to_set => [0x00f0],
92
+ [:alveolar, :fricative].to_set => [0x0073],
93
+ [:voiced, :alveolar, :fricative].to_set => [0x007a],
94
+ [:postalveolar, :fricative].to_set => [0x0283],
95
+ [:voiced, :postalveolar, :fricative].to_set => [0x0292],
96
+ [:retroflex, :fricative].to_set => [0x0282],
97
+ [:voiced, :retroflex, :fricative].to_set => [0x0290],
98
+ [:palatal, :fricative].to_set => [0x00e7],
99
+ [:voiced, :palatal, :fricative].to_set => [0x029d],
100
+ [:velar, :fricative].to_set => [0x0078],
101
+ [:voiced, :velar, :fricative].to_set => [0x0263],
102
+ [:uvular, :fricative].to_set => [0x03c7],
103
+ [:voiced, :uvular, :fricative, :approximant].to_set => [0x0281],
104
+ [:pharyngeal, :fricative].to_set => [0x0127],
105
+ [:voiced, :pharyngeal, :fricative, :approximant].to_set => [0x0295],
106
+ [:epiglottal, :fricative].to_set => [0x029c],
107
+ [:voiced, :epiglottal, :fricative, :approximant].to_set => [0x02a2],
108
+ [:glottal, :fricative, :approximant].to_set => [0x0068],
109
+ [:voiced, :glottal, :fricative, :approximant].to_set => [0x0266],
110
+ [:voiced, :bilabial, :approximant].to_set => [0x03b2, 0x031e],
111
+ [:voiced, :labiodental, :approximant].to_set => [0x028b],
112
+ [:voiced, :alveolar, :approximant].to_set => [0x0279],
113
+ [:voiced, :retroflex, :approximant].to_set => [0x027b],
114
+ [:voiced, :palatal, :approximant].to_set => [0x006a],
115
+ [:voiced, :velar, :approximant].to_set => [0x0270],
116
+ [:voiced, :bilabial, :trill].to_set => [0x0299],
117
+ [:voiced, :alveolar, :trill].to_set => [0x0072],
118
+ [:voiced, :uvular, :trill].to_set => [0x0280],
119
+ [:voiced, :epiglottal, :trill].to_set => [0x044f],
120
+ [:voiced, :bilabial, :flap].to_set => [0x2c71, 0x031f],
121
+ [:voiced, :labiodental, :flap].to_set => [0x2c71],
122
+ [:voiced, :alveolar, :flap].to_set => [0x027e],
123
+ [:voiced, :retroflex, :flap].to_set => [0x027d],
124
+ [:voiced, :uvular, :flap].to_set => [0x0262, 0x0306],
125
+ [:voiced, :epiglottal, :flap].to_set => [0x02a1, 0x032f],
126
+ [:alveolar, :lateral_fricative].to_set => [0x026c],
127
+ [:voiced, :alveolar, :lateral_fricative].to_set => [0x026e],
128
+ [:voiced, :alveolar, :lateral_approximant].to_set => [0x006c],
129
+ [:voiced, :retroflex, :lateral_approximant].to_set => [0x026d],
130
+ [:voiced, :palatal, :lateral_approximant].to_set => [0x028e],
131
+ [:voiced, :velar, :lateral_approximant].to_set => [0x029f],
132
+ [:voiced, :alveolar, :lateral_flap].to_set => [0x027a],
133
+ [:voiced, :palatal, :lateral_flap].to_set => [0x028e, 0x032f],
134
+ [:voiced, :velar, :lateral_flap].to_set => [0x029f, 0x0306],
135
+ [:voiced, :close, :front].to_set => [0x0069],
136
+ [:voiced, :close, :front, :rounded].to_set => [0x0079],
137
+ [:voiced, :close, :central].to_set => [0x0268],
138
+ [:voiced, :close, :central, :rounded].to_set => [0x0289],
139
+ [:voiced, :close, :back].to_set => [0x026f],
140
+ [:voiced, :close, :back, :rounded].to_set => [0x0075],
141
+ [:voiced, :near_close, :near_front].to_set => [0x026a],
142
+ [:voiced, :near_close, :near_front, :rounded].to_set => [0x028f],
143
+ [:voiced, :near_close, :near_back, :rounded].to_set => [0x028a],
144
+ [:voiced, :close_mid, :front].to_set => [0x0065],
145
+ [:voiced, :close_mid, :front, :rounded].to_set => [0x00f8],
146
+ [:voiced, :close_mid, :central].to_set => [0x0258],
147
+ [:voiced, :close_mid, :central, :rounded].to_set => [0x0275],
148
+ [:voiced, :close_mid, :back].to_set => [0x0264],
149
+ [:voiced, :close_mid, :back, :rounded].to_set => [0x006f],
150
+ [:voiced, :mid, :central].to_set => [0x0259],
151
+ [:voiced, :open_mid, :front].to_set => [0x025b],
152
+ [:voiced, :open_mid, :front, :rounded].to_set => [0x0153],
153
+ [:voiced, :open_mid, :central].to_set => [0x025c],
154
+ [:voiced, :open_mid, :central, :rounded].to_set => [0x025e],
155
+ [:voiced, :open_mid, :back].to_set => [0x028c],
156
+ [:voiced, :open_mid, :back, :rounded].to_set => [0x0254],
157
+ [:voiced, :near_open, :front].to_set => [0x00e6],
158
+ [:voiced, :near_open, :central].to_set => [0x0250],
159
+ [:voiced, :open, :front].to_set => [0x0061],
160
+ [:voiced, :open, :front, :rounded].to_set => [0x0276],
161
+ [:voiced, :open, :back].to_set => [0x0251],
162
+ [:voiced, :open, :back, :rounded].to_set => [0x0252],
163
+ [:voiced, :retroflex, :lateral_fricative].to_set => [0xa78e],
164
+ [:voiced, :palatal, :lateral_fricative].to_set => [0xf267],
165
+ [:voiced, :velar, :lateral_fricative].to_set => [0xf268],
166
+ [:voiced, :retroflex, :lateral_flap].to_set => [0xf269],
167
+ [:voiced, :bilabial, :velar, :approximant].to_set => [0x0077],
168
+ [:bilabial, :velar, :approximant].to_set => [0x268d],
169
+ [:voiced, :bilabial, :palatal, :approximant].to_set => [0x0265],
170
+ [:voiced, :velar, :alveolar, :lateral_approximant].to_set => [0x026b],
171
+ [:palatal, :alveolar, :fricative].to_set => [0x0255],
172
+ [:voiced, :palatal, :alveolar, :fricative].to_set => [0x0291],
173
+ [:palatal, :velar, :fricative].to_set => [0x0267]
160
174
  }
175
+ SETS.each {|k, v| k.freeze}
161
176
 
162
177
  class << self
163
178
  # Return the set corresponding to the kind of feature: manner or place of
@@ -171,11 +186,19 @@ module Phonology
171
186
  # corresponding features. If a single feature is given, just return the
172
187
  # feature.
173
188
  def expand(*args)
174
- args = args.inject([].to_set) do |memo, obj|
189
+ args = args.first.kind_of?(Set) ? args.shift.to_a : args.flatten
190
+ args.inject([].to_set) do |memo, obj|
175
191
  memo << (CLASSES[obj] || obj)
176
192
  end.flatten.to_set
177
193
  end
178
194
 
195
+ def place_groups(*args)
196
+ args = args.first.kind_of?(Set) ? args.shift : args.flatten.to_set
197
+ [:labial, :coronal, :dorsal, :radical].map do |fclass|
198
+ fclass unless (CLASSES[fclass] & args).empty?
199
+ end.compact.uniq
200
+ end
201
+
179
202
  end
180
203
 
181
204
  end
@@ -1,7 +1,7 @@
1
1
  module Phonology
2
2
 
3
3
  # An inventory of Phonological feature sets.
4
- class Sounds
4
+ class Inventory
5
5
 
6
6
  # A Hash with a set of features as keys, and UTF-8 codepoints for IPA
7
7
  # letters as values.
@@ -32,38 +32,58 @@ module Phonology
32
32
  end
33
33
 
34
34
  # Given a set of features, return an array of UTF-8 codepoints.
35
- def codepoints(features)
36
- features = features.to_set.flatten
35
+ def codepoints(*features)
36
+ features = setify(*features)
37
37
  @sets[features] || (raise FeatureError, "No such set #{features.inspect}")
38
38
  end
39
39
 
40
40
  # Return an instance of Sounds whose sets include any of the given
41
41
  # features.
42
42
  def with(*features)
43
- features = features.to_set
44
- self.class.new @sets.select {|key, val| !key.intersection(features).empty?}
43
+ pos, neg = mangle_args(*features)
44
+ self.class.new(@sets.select do |key, val|
45
+ !key.intersection(pos).empty?
46
+ end).without_any(neg)
45
47
  end
46
48
 
47
49
  # Return feature sets that include all of the given features
48
50
  def with_all(*features)
49
- features = features.to_set
50
- self.class.new @sets.select {|key, val| features.subset?(key)}
51
+ pos, neg = mangle_args(*features)
52
+ self.class.new(@sets.select do |key, val|
53
+ pos.subset?(key)
54
+ end).without_any(neg)
51
55
  end
52
56
 
53
57
  # Return an instance of Sounds whose sets exclude any of the given
54
58
  # features.
55
59
  def without(*features)
56
- features = features.to_set
60
+ features = setify(*features)
57
61
  self.class.new @sets.select {|key, val| !features.subset?(key)}
58
62
  end
59
63
 
60
64
  # Return an instance of Sounds whose sets exclude all of the given
61
65
  # features.
62
66
  def without_any(*features)
63
- features = features.to_set
67
+ features = setify(*features)
64
68
  self.class.new @sets.select {|key, val| key.intersection(features).empty?}
65
69
  end
66
70
 
71
+ private
72
+
73
+ def mangle_args(*args)
74
+ pos = setify(*args)
75
+ neg = extract_negations(pos)
76
+ [pos.select {|f| f !~ /\A(non_|un)/}.compact.to_set, neg]
77
+ end
78
+
79
+ def setify(*args)
80
+ Features.expand(args.first.kind_of?(Set) ? args.shift : args.flatten.to_set)
81
+ end
82
+
83
+ def extract_negations(set)
84
+ Features.expand(*set.map {|feat| feat =~ /\A(non_|un)([a-z_]*)\z/ && $2.to_sym}.compact)
85
+ end
86
+
67
87
  end
68
88
 
69
89
  end
@@ -0,0 +1,117 @@
1
+ module Phonology
2
+
3
+ module OrthographyTranslatorDSL
4
+
5
+ attr_accessor :last_sound, :anticipation
6
+
7
+ def method_missing(sym, *args, &block)
8
+ last_sound.__send__(sym, *args, &block)
9
+ end
10
+
11
+ def anticipate(&block)
12
+ @anticipation = block
13
+ nil
14
+ end
15
+
16
+ def curr_char
17
+ array[@index]
18
+ end
19
+
20
+ def next_char(offset = 1)
21
+ array[@index + offset]
22
+ end
23
+
24
+ def prev_char(offset = 1)
25
+ array[@index - offset] unless @index == 0
26
+ end
27
+
28
+ def follows(*chars)
29
+ chars.flatten.each {|c| return true if c == prev_char}
30
+ false
31
+ end
32
+
33
+ def precedes(*chars)
34
+ return false if !next_char
35
+ chars.flatten.each {|c| return true if c == next_char}
36
+ false
37
+ end
38
+
39
+ def between(before, after)
40
+ follows(*before) && precedes(*after)
41
+ end
42
+
43
+ def initial?
44
+ @index == 0
45
+ end
46
+
47
+ def final?
48
+ !next_char
49
+ end
50
+
51
+ def get(*features)
52
+ if features.first.kind_of?(Array)
53
+ get_affricate(*features)
54
+ else
55
+ match = sounds.with_all(*features)
56
+ return nil if match.sets.empty? || match.sets.length > 1
57
+ @last_sound = Sound.new *match.sets.keys.first.to_a
58
+ end
59
+ @last_sound.orthography = curr_char
60
+ do_anticipation
61
+ end
62
+
63
+ private
64
+
65
+ def get_affricate(*features)
66
+ @last_sound = Phonology::Affricate.new(features[0], features[1])
67
+ end
68
+
69
+ def do_anticipation
70
+ if @anticipation
71
+ @anticipation.call(last_sound)
72
+ @anticipation = nil
73
+ end
74
+ return last_sound
75
+ end
76
+
77
+ end
78
+
79
+ class OrthographyTranslator
80
+
81
+ attr_accessor :rules, :scanner, :sounds, :string
82
+
83
+ include OrthographyTranslatorDSL
84
+
85
+ # Translate orthorgraphy to IPA
86
+ def translate(string)
87
+ @string = string
88
+ @max = array.length
89
+ SoundSequence.new(array.each_index.map do |index|
90
+ @index = index
91
+ instance_eval(&@rules)
92
+ end.flatten.compact)
93
+ ensure
94
+ @max = 0
95
+ @string = nil
96
+ @array = nil
97
+ @index = nil
98
+ @last_sound = nil
99
+ end
100
+
101
+ def set_rules(&block)
102
+ @rules = block
103
+ end
104
+
105
+ private
106
+
107
+ def array
108
+ @array ||= scan
109
+ end
110
+
111
+ def scan
112
+ scanner ? scanner.call(string) : string.split(//u)
113
+ end
114
+
115
+ end
116
+
117
+ end
@@ -0,0 +1,103 @@
1
+ module Phonology
2
+
3
+ class RulesDSL
4
+
5
+ attr :array, :index
6
+
7
+ def initialize(array)
8
+ @array = array
9
+ end
10
+
11
+ def method_missing(sym, *args, &block)
12
+ curr_sound.__send__(sym, *args, &block)
13
+ end
14
+
15
+ def apply(&block)
16
+ array.each_index.map do |index|
17
+ @index = index
18
+ instance_eval(&block)
19
+ get_result
20
+ end
21
+ end
22
+
23
+ def delete(*args)
24
+ if !args.empty?
25
+ curr_sound.delete(*args)
26
+ else
27
+ array[@index] = nil
28
+ end
29
+ end
30
+
31
+ def insert(*args)
32
+ @result = [curr_sound, Sound.new(*args)]
33
+ end
34
+
35
+ def curr_sound
36
+ array[@index]
37
+ end
38
+
39
+ def next_sound(offset = 1)
40
+ !final? && array[@index + offset]
41
+ end
42
+
43
+ def prev_sound(offset = 1)
44
+ !initial? && array[@index - offset]
45
+ end
46
+
47
+ def voice
48
+ curr_sound << :voiced
49
+ end
50
+
51
+ def devoice
52
+ curr_sound >> :voiced
53
+ end
54
+
55
+ def precedes(*features)
56
+ return false if !next_sound
57
+ features.flatten.each {|f| return false unless next_sound.send(:"#{f}?")}
58
+ true
59
+ end
60
+
61
+ def follows(*features)
62
+ return false if initial?
63
+ features.flatten.each {|f| return false unless prev_sound.send(:"#{f}?")}
64
+ true
65
+ end
66
+
67
+ def initial?
68
+ @index == 0
69
+ end
70
+
71
+ def final?
72
+ @index == @max
73
+ end
74
+
75
+ private
76
+
77
+ def get_result
78
+ if !@result
79
+ curr_sound
80
+ else
81
+ result = @result
82
+ @result = nil
83
+ result
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+ class Rule
90
+
91
+ attr :rule
92
+
93
+ def initialize(&block)
94
+ @rule = block
95
+ end
96
+
97
+ def apply(sounds)
98
+ dsl = RulesDSL.new(sounds)
99
+ dsl.apply(&rule).flatten
100
+ end
101
+
102
+ end
103
+ end
@@ -1,22 +1,18 @@
1
1
  module Phonology
2
2
 
3
- # A set of distinctive features
4
- class Sound
3
+ module SoundBase
5
4
 
6
- attr_reader :features
7
-
8
- def initialize(*features)
9
- @features = if features.first.kind_of?(String)
10
- Phonology.features(features.shift)
11
- else
12
- features.to_set
13
- end
14
- end
5
+ attr_accessor :features, :orthography, :hints
6
+ protected :features=, :hints=
15
7
 
16
8
  Features::ALL.each do |feature|
17
9
  class_eval(<<-EOM, __FILE__, __LINE__ +1)
18
10
  def #{feature}?
19
- @features.include? :#{feature}
11
+ features.include? :#{feature}
12
+ end
13
+
14
+ def non_#{feature}?
15
+ !#{feature}?
20
16
  end
21
17
  EOM
22
18
  end
@@ -25,79 +21,176 @@ module Phonology
25
21
  class_eval(<<-EOM, __FILE__, __LINE__ +1)
26
22
  def #{feature_class}?
27
23
  set = Features.expand(:#{feature_class})
28
- !set.intersection(@features).empty?
24
+ !set.intersection(features).empty?
25
+ end
26
+
27
+ def non_#{feature_class}?
28
+ !#{feature_class}?
29
29
  end
30
30
  EOM
31
31
  end
32
32
 
33
- # Get the IPA symbol for the sound.
34
- def symbol
35
- Phonology.symbol(features)
33
+ alias unvoiced? non_voiced?
34
+
35
+ def sonority
36
+ case
37
+ when vocalic? then 5
38
+ when approximant? then 4
39
+ when liquid? then 3
40
+ when nasal? then 2
41
+ when fricative? then 1
42
+ else 0
43
+ end
36
44
  end
37
45
 
38
- # Add a feature, replacing either the place or manner of articulation,
39
- # or the height or backness. Returns self.
40
- def <<(feature)
41
- feature = feature.to_sym
42
- (@features -= (Features.set(feature) || [])) << feature
43
- self
46
+ # Orthography hints that can be useful to consult when applying rules.
47
+ def hints
48
+ @hints ||= []
44
49
  end
45
- alias add <<
46
50
 
47
- # Remove a feature, and return self.
48
- def >>(feature)
49
- @features -= [feature.to_sym]
51
+ # TODO set up list of valid hints
52
+ def hint(*args)
53
+ self.hints += args.flatten
50
54
  self
51
55
  end
52
56
 
53
- # Get the next sound, moving backwards in the mouth.
54
- def backward
55
- if consonantal?
56
- index = Features::PLACE.index(place.first)
57
- max = Features::PLACE.length
58
- features = @features - Features::PLACE
59
- until index > max do
60
- index = index + 1
61
- feature = Features::PLACE[index]
62
- if self.class.exists?(features + [feature])
63
- @features = features + [feature]
64
- return self
65
- end
66
- end
67
- end
68
- self
57
+ def orthography
58
+ @orthography ||= ""
59
+ end
60
+
61
+ def codepoints
62
+ raise NotImplementedError
63
+ end
64
+
65
+ def exists?
66
+ raise NotImplementedError
69
67
  end
70
68
 
71
- def forward
69
+ # Get the IPA codepoints for the sound, including any diacritics.
70
+ def symbol
71
+ codepoints.pack("U*")
72
+ end
73
+
74
+ # Does the sound have more than one place of articulation?
75
+ def coarticulated?
76
+ place.size > 1
72
77
  end
73
78
 
74
79
  # Get the sound's place of articulation.
75
80
  def place
76
- @features.intersection(Features::PLACE)
81
+ features.intersection(Features::PLACE)
77
82
  end
78
83
 
79
84
  # Get the sound's manner of articulation.
80
85
  def manner
81
- @features.intersection(Features::MANNER)
86
+ features.intersection(Features::MANNER)
82
87
  end
83
88
 
84
89
  # Get the sound's height.
85
90
  def height
86
- @features.intersection(Features::HEIGHT)
91
+ features.intersection(Features::HEIGHT)
87
92
  end
88
93
 
89
94
  # Get the sound's backness.
90
95
  def backness
91
- @features.intersection(Features::BACKNESS)
96
+ features.intersection(Features::BACKNESS)
92
97
  end
93
98
 
94
- def exists?
95
- self.class.exists?(@features)
99
+ # Get the place groups (:coronal, :dorsal, etc). Normally there should only be one.
100
+ def place_groups
101
+ Features.place_groups(features)
96
102
  end
97
103
 
104
+ end
105
+
106
+ # A set of distinctive features
107
+ class Sound
108
+
109
+ include SoundBase
110
+
111
+ # Does the group of features exist in human speech?
98
112
  def self.exists?(features)
99
113
  !! Features::SETS[features]
100
114
  end
101
115
 
116
+ def initialize(*features)
117
+ if features.first.kind_of?(String)
118
+ self.features = Phonology.features_for(features.shift).clone
119
+ else
120
+ self.features = features.to_set
121
+ end
122
+ end
123
+
124
+ # Get the IPA codepoints for the sound, excluding any diacritics.
125
+ def codepoints
126
+ Phonology.sounds.codepoints(features)
127
+ end
128
+
129
+ # Add a feature, replacing either the place or manner of articulation,
130
+ # or the height or backness. Returns self.
131
+ def <<(*args)
132
+ args.to_a.flatten.each do |feature|
133
+ features.subtract Features.set(feature).to_a
134
+ add! feature
135
+ end
136
+ self
137
+ end
138
+ alias add <<
139
+
140
+ # Add a feature without replacing place or manner.
141
+ def add!(feature)
142
+ features.add feature.to_sym
143
+ self
144
+ end
145
+
146
+ # Remove a feature, and return self.
147
+ def >>(*args)
148
+ args.to_a.flatten.each do |feature|
149
+ features.delete feature.to_sym
150
+ end
151
+ self
152
+ end
153
+ alias delete >>
154
+
155
+ # Does the sound exist?
156
+ def exists?
157
+ self.class.exists?(features)
158
+ end
159
+
160
+ end
161
+
162
+
163
+ # A sound which begins plosive and finished fricative.
164
+ class Affricate
165
+
166
+ include SoundBase
167
+
168
+ attr_accessor :onset, :release
169
+ protected :onset=, :release=
170
+
171
+ def initialize(onset, release)
172
+ self.onset = get_sound(onset)
173
+ self.release = get_sound(release)
174
+ end
175
+
176
+ def exists?
177
+ onset.exists? && release.exists?
178
+ end
179
+
180
+ def features
181
+ onset.features + release.features
182
+ end
183
+
184
+ def codepoints
185
+ [onset.codepoints, Features::DIACRITICS[:affricate], release.codepoints].flatten
186
+ end
187
+
188
+ private
189
+
190
+ def get_sound(arg)
191
+ arg.kind_of?(Sound) ? arg : Sound.new(*arg)
192
+ end
193
+
102
194
  end
195
+
103
196
  end
@@ -0,0 +1,45 @@
1
+ module Phonology
2
+
3
+ # A collection of sounds
4
+ class SoundSequence
5
+
6
+ attr :sounds
7
+
8
+ def method_missing(sym, *args, &block)
9
+ @sounds.__send__(sym, *args, &block)
10
+ end
11
+
12
+ def initialize(arg)
13
+ # String of ipa symbols
14
+ if arg.kind_of? String
15
+ @sounds = arg.split('').map {|letter| Sound.new(letter)}
16
+ else
17
+ @sounds = arg.to_a
18
+ end
19
+ end
20
+
21
+ def symbols
22
+ compact.map(&:symbol).join
23
+ end
24
+ alias ipa symbols
25
+ alias to_s symbols
26
+ alias to_str symbols
27
+
28
+ def orthography
29
+ compact.map(&:orthography).join
30
+ end
31
+
32
+ # Apply rule and get a new instance of SoundSequence.
33
+ def apply_rule(rule)
34
+ self.class.new rule.apply(@sounds)
35
+ end
36
+
37
+ # Apply rule in place.
38
+ def apply_rule!(rule)
39
+ @sounds = rule.apply(@sounds)
40
+ self
41
+ end
42
+
43
+
44
+ end
45
+ end
@@ -1,3 +1,3 @@
1
1
  module Phonology
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.5"
3
3
  end
data/lib/phonology.rb CHANGED
@@ -1,21 +1,24 @@
1
1
  require "set"
2
2
  require File.expand_path("../phonology/features", __FILE__)
3
- require File.expand_path("../phonology/sounds", __FILE__)
3
+ require File.expand_path("../phonology/inventory", __FILE__)
4
4
  require File.expand_path("../phonology/sound", __FILE__)
5
+ require File.expand_path("../phonology/sound_sequence", __FILE__)
6
+ require File.expand_path("../phonology/orthography", __FILE__)
7
+ require File.expand_path("../phonology/rule", __FILE__)
5
8
 
6
9
  module Phonology
7
10
 
8
11
  extend self
9
12
 
10
13
  def sounds
11
- @sounds ||= Sounds.new Features::SETS
14
+ @sounds ||= Inventory.new(Features::SETS).freeze
12
15
  end
13
16
 
14
- def symbol(*args)
17
+ def symbol_for(*args)
15
18
  sounds.symbol(*args)
16
19
  end
17
20
 
18
- def features(*args)
21
+ def features_for(*args)
19
22
  sounds.features(*args)
20
23
  end
21
24
 
@@ -0,0 +1,21 @@
1
+ require File.expand_path("../test_helper", __FILE__)
2
+
3
+ class AffricateTest < Test::Unit::TestCase
4
+ include Phonology
5
+
6
+ def setup
7
+ @affricate = Affricate.new("t", "s")
8
+ end
9
+
10
+ test "should get symbol for affricate" do
11
+ assert_equal [116, 865, 115], @affricate.symbol.unpack("U*")
12
+ end
13
+
14
+ test "should act like a sound" do
15
+ assert @affricate.exists?
16
+ assert @affricate.coronal?
17
+ assert @affricate.plosive?
18
+ assert @affricate.fricative?
19
+ end
20
+
21
+ end
@@ -2,9 +2,17 @@ require File.expand_path("../test_helper", __FILE__)
2
2
 
3
3
  class FeaturesTest < Test::Unit::TestCase
4
4
 
5
- test "should expand feature groups" do
5
+ include Phonology
6
+
7
+ test "should expand place groups" do
6
8
  assert_equal [:dental, :alveolar, :postalveolar, :retroflex, :velar].to_set,
7
- Phonology::Features.expand(:coronal, :dental, :velar)
9
+ Features.expand(:coronal, :dental, :velar)
10
+ end
11
+
12
+ test "should contract place groups" do
13
+ assert_equal [:coronal], Features.place_groups([:alveolar, :plosive, :postalveolar, :fricative])
14
+ assert_equal [:coronal], Features.place_groups([:alveolar, :plosive, :postalveolar, :fricative].to_set)
15
+ assert_equal [:coronal], Features.place_groups(:alveolar, :plosive, :postalveolar, :fricative)
8
16
  end
9
17
 
10
18
  end
@@ -7,7 +7,7 @@ class SoundsTest < Test::Unit::TestCase
7
7
  end
8
8
 
9
9
  def small_set
10
- Phonology::Sounds.from_ipa("b", "p", "d", "t", "m", "n")
10
+ Phonology::Inventory.from_ipa("b", "p", "d", "t", "m", "n")
11
11
  end
12
12
 
13
13
  test "should return feature set for symbol" do
@@ -29,4 +29,17 @@ class SoundsTest < Test::Unit::TestCase
29
29
  test "#without_any should return a set of sets that exclude all of the given features" do
30
30
  assert_equal ["d", "t"].to_set, small_set.without_any(:bilabial, :nasal).symbols
31
31
  end
32
+
33
+ test "should expand features" do
34
+ assert_equal ["t", "d", "n"].to_set, small_set.with(:coronal).symbols
35
+ end
36
+
37
+ test "#with should exclude negative features" do
38
+ assert_equal ["t", "d"].to_set, small_set.with(:coronal, :non_nasal).symbols
39
+ end
40
+
41
+ test "#with_all should exclude negative features" do
42
+ assert_equal ["t"].to_set, small_set.with_all(:alveolar, :unvoiced).symbols
43
+ end
44
+
32
45
  end
@@ -2,16 +2,16 @@ require File.expand_path("../test_helper", __FILE__)
2
2
 
3
3
  class PhonologyTest < Test::Unit::TestCase
4
4
 
5
- test "sounds should return an instance of Sounds" do
6
- assert_equal Phonology::Sounds, Phonology.sounds.class
5
+ test "sounds should return an instance of Inventory" do
6
+ assert_equal Phonology::Inventory, Phonology.sounds.class
7
7
  end
8
8
 
9
9
  test "should return feature set for symbol" do
10
- assert_equal [:voiced, :bilabial, :nasal].to_set, Phonology.features("m")
10
+ assert_equal [:voiced, :bilabial, :nasal].to_set, Phonology.features_for("m")
11
11
  end
12
12
 
13
13
  test "should return a symbol for a feature set" do
14
- assert_equal "d", Phonology.symbol(:voiced, :alveolar, :plosive)
14
+ assert_equal "d", Phonology.symbol_for(:voiced, :alveolar, :plosive)
15
15
  end
16
16
 
17
17
  end
data/test/rule_test.rb ADDED
@@ -0,0 +1,40 @@
1
+ require File.expand_path("../test_helper", __FILE__)
2
+
3
+ class RuleTest < Test::Unit::TestCase
4
+
5
+ include Phonology
6
+
7
+ test "should delegate to current sound" do
8
+ assert_rule "amta", "anta" do
9
+ add :alveolar if nasal? and precedes :alveolar
10
+ end
11
+ end
12
+
13
+ test "should delete sound" do
14
+ assert_rule "amta", "ama" do
15
+ delete if alveolar? and follows :nasal
16
+ end
17
+ end
18
+
19
+ test "should insert sound" do
20
+ assert_rule "pta", "pata" do
21
+ insert "a" if plosive? and precedes :plosive
22
+ end
23
+ end
24
+
25
+ test "should devoice" do
26
+ assert_rule "bta", "pta" do
27
+ devoice if plosive? and precedes(:plosive, :unvoiced)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def assert_rule(given, expected, &block)
34
+ rule = Rule.new &block
35
+ sounds = SoundSequence.new(given)
36
+ assert_equal expected, sounds.apply_rule!(rule).to_s
37
+ end
38
+
39
+
40
+ end
data/test/sound_test.rb CHANGED
@@ -4,6 +4,22 @@ class SoundTest < Test::Unit::TestCase
4
4
 
5
5
  include Phonology
6
6
 
7
+ test "vowels should be more sonorous than approximants" do
8
+ assert Sound.new("o").sonority > Sound.new("w").sonority
9
+ end
10
+
11
+ test "approximants should be more sonorous than liquids" do
12
+ assert Sound.new("j").sonority > Sound.new("l").sonority
13
+ end
14
+
15
+ test "liquids should be more sonorous than nasals" do
16
+ assert Sound.new("l").sonority > Sound.new("n").sonority
17
+ end
18
+
19
+ test "nasals should be more sonorous than non-nasal consonants" do
20
+ assert Sound.new("n").sonority > Sound.new("z").sonority
21
+ end
22
+
7
23
  test "should return an IPA symbol" do
8
24
  assert_equal "m", Sound.new(:voiced, :bilabial, :nasal).symbol
9
25
  end
@@ -60,6 +76,10 @@ class SoundTest < Test::Unit::TestCase
60
76
  assert_equal :alveolar, Sound.new("r").place.first
61
77
  end
62
78
 
79
+ test "should indicate if sound is coarticulated" do
80
+ assert Sound.new("w").coarticulated?
81
+ end
82
+
63
83
  test "should get manner of articulation" do
64
84
  assert_equal :trill, Sound.new("r").manner.first
65
85
  end
@@ -72,13 +92,9 @@ class SoundTest < Test::Unit::TestCase
72
92
  assert_equal :front, Sound.new("e").backness.first
73
93
  end
74
94
 
75
- test "should indicate if sound exists" do
95
+ test "should indicate if simple sound exists" do
76
96
  assert !Sound.new(:front, :plosive).exists?
77
97
  assert Sound.new(:bilabial, :plosive).exists?
78
98
  end
79
99
 
80
- test "should move sounds backwards" do
81
- assert_equal "n", Sound.new("m").backward.backward.symbol
82
- end
83
-
84
100
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 1
9
- version: 0.0.1
8
+ - 5
9
+ version: 0.0.5
10
10
  platform: ruby
11
11
  authors:
12
12
  - Norman Clarke
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-05-06 00:00:00 -03:00
17
+ date: 2010-05-20 00:00:00 -03:00
18
18
  default_executable:
19
19
  dependencies: []
20
20
 
@@ -29,13 +29,18 @@ extra_rdoc_files: []
29
29
  files:
30
30
  - lib/phonology.rb
31
31
  - lib/phonology/features.rb
32
+ - lib/phonology/inventory.rb
33
+ - lib/phonology/orthography.rb
34
+ - lib/phonology/rule.rb
32
35
  - lib/phonology/sound.rb
33
- - lib/phonology/sounds.rb
36
+ - lib/phonology/sound_sequence.rb
34
37
  - lib/phonology/version.rb
38
+ - test/affricate_test.rb
35
39
  - test/features_test.rb
40
+ - test/inventory_test.rb
36
41
  - test/phonology_test.rb
42
+ - test/rule_test.rb
37
43
  - test/sound_test.rb
38
- - test/sounds_test.rb
39
44
  - test/test_helper.rb
40
45
  - README.md
41
46
  - LICENSE