phonology 0.0.1 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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