Skip to content

Commit 940a1fe

Browse files
committed
Add Ukrainian stemmer
1 parent 8fd4138 commit 940a1fe

File tree

2 files changed

+352
-0
lines changed

2 files changed

+352
-0
lines changed

algorithms/ukrainian.sbl

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
stringescapes {}
2+
3+
/* the 33 Ukrainian letters and apostrophe represented by single quote */
4+
5+
stringdef a '{U+0430}'
6+
stringdef b '{U+0431}'
7+
stringdef v '{U+0432}'
8+
stringdef gh '{U+0433}'
9+
stringdef g '{U+0491}'
10+
stringdef d '{U+0434}'
11+
stringdef e '{U+0435}'
12+
stringdef ye '{U+0454}'
13+
stringdef zh '{U+0436}'
14+
stringdef z '{U+0437}'
15+
stringdef y '{U+0438}'
16+
stringdef i '{U+0456}'
17+
stringdef yi '{U+0457}'
18+
stringdef i` '{U+0439}'
19+
stringdef k '{U+043A}'
20+
stringdef l '{U+043B}'
21+
stringdef m '{U+043C}'
22+
stringdef n '{U+043D}'
23+
stringdef o '{U+043E}'
24+
stringdef p '{U+043F}'
25+
stringdef r '{U+0440}'
26+
stringdef s '{U+0441}'
27+
stringdef t '{U+0442}'
28+
stringdef u '{U+0443}'
29+
stringdef f '{U+0444}'
30+
stringdef kh '{U+0445}'
31+
stringdef ts '{U+0446}'
32+
stringdef ch '{U+0447}'
33+
stringdef sh '{U+0448}'
34+
stringdef shch '{U+0449}'
35+
stringdef soft '{U+044C}'
36+
stringdef iu '{U+044E}'
37+
stringdef ia '{U+044F}'
38+
stringdef apostrophe '{U+0027}'
39+
40+
routines ( mark_regions R2
41+
// perfective_gerund
42+
adjective
43+
adjectival
44+
// reflexive
45+
// verb
46+
// noun
47+
// derivational
48+
// tidy_up
49+
)
50+
51+
externals ( stem )
52+
53+
integers ( pV p2 )
54+
55+
groupings ( v )
56+
57+
define v '{a}{e}{ye}{y}{i}{yi}{o}{u}{iu}{ia}'
58+
59+
define mark_regions as (
60+
61+
$pV = limit
62+
$p2 = limit
63+
do (
64+
gopast v setmark pV gopast non-v
65+
gopast v gopast non-v setmark p2
66+
)
67+
)
68+
69+
backwardmode (
70+
71+
define R2 as $p2 <= cursor
72+
73+
// define perfective_gerund as (
74+
// [substring] among (
75+
// // '{v}'
76+
// // '{v}{sh}{i}'
77+
// // '{v}{sh}{i}{s}{'}'
78+
// // ('{a}' or '{ia}' delete)
79+
// // '{i}{v}'
80+
// // '{i}{v}{sh}{i}'
81+
// // '{i}{v}{sh}{i}{s}{'}'
82+
// // '{y}{v}'
83+
// // '{y}{v}{sh}{i}'
84+
// // '{y}{v}{sh}{i}{s}{'}'
85+
// // (delete)
86+
// )
87+
// )
88+
89+
define adjective as (
90+
[substring] among (
91+
'{soft}{o}{gh}{o}'
92+
(delete)
93+
// '{e}{e}' '{i}{e}' '{y}{e}' '{o}{e}' '{i}{m}{i}' '{y}{m}{i}'
94+
// '{e}{i`}' '{i}{i`}' '{y}{i`}' '{o}{i`}' '{e}{m}' '{i}{m}'
95+
// '{y}{m}' '{o}{m}' '{e}{g}{o}' '{o}{g}{o}' '{e}{m}{u}'
96+
// '{o}{m}{u}' '{i}{kh}' '{y}{kh}' '{u}{iu}' '{iu}{iu}' '{a}{ia}'
97+
// '{ia}{ia}'
98+
// // and -
99+
// '{o}{iu}' // - which is somewhat archaic
100+
// '{e}{iu}' // - soft form of {o}{iu}
101+
// (delete)
102+
)
103+
)
104+
105+
define adjectival as (
106+
adjective
107+
108+
/* of the participle forms, em, vsh, ivsh, yvsh are readily removable.
109+
nn, {iu}shch, shch, u{iu}shch can be removed, with a small proportion of
110+
errors. Removing im, uem, enn creates too many errors.
111+
*/
112+
113+
// try (
114+
// [substring] among (
115+
// // '{e}{m}' // present passive participle
116+
// // '{n}{n}' // adjective from past passive participle
117+
// // '{v}{sh}' // past active participle
118+
// // '{iu}{shch}' '{shch}' // present active participle
119+
// // ('{a}' or '{ia}' delete)
120+
121+
// // //but not '{i}{m}' '{u}{e}{m}' // present passive participle
122+
// // //or '{e}{n}{n}' // adjective from past passive participle
123+
124+
// // '{i}{v}{sh}' '{y}{v}{sh}'// past active participle
125+
// // '{u}{iu}{shch}' // present active participle
126+
// // (delete)
127+
// )
128+
// )
129+
130+
)
131+
132+
// define reflexive as (
133+
// [substring] among (
134+
// // '{s}{ia}'
135+
// // '{s}{'}'
136+
// // (delete)
137+
// )
138+
// )
139+
140+
// define verb as (
141+
// [substring] among (
142+
// // '{l}{a}' '{n}{a}' '{e}{t}{e}' '{i`}{t}{e}' '{l}{i}' '{i`}'
143+
// // '{l}' '{e}{m}' '{n}' '{l}{o}' '{n}{o}' '{e}{t}' '{iu}{t}'
144+
// // '{n}{y}' '{t}{'}' '{e}{sh}{'}'
145+
146+
// // '{n}{n}{o}'
147+
// // ('{a}' or '{ia}' delete)
148+
149+
// // '{i}{l}{a}' '{y}{l}{a}' '{e}{n}{a}' '{e}{i`}{t}{e}'
150+
// // '{u}{i`}{t}{e}' '{i}{t}{e}' '{i}{l}{i}' '{y}{l}{i}' '{e}{i`}'
151+
// // '{u}{i`}' '{i}{l}' '{y}{l}' '{i}{m}' '{y}{m}' '{e}{n}'
152+
// // '{i}{l}{o}' '{y}{l}{o}' '{e}{n}{o}' '{ia}{t}' '{u}{e}{t}'
153+
// // '{u}{iu}{t}' '{i}{t}' '{y}{t}' '{e}{n}{y}' '{i}{t}{'}'
154+
// // '{y}{t}{'}' '{i}{sh}{'}' '{u}{iu}' '{iu}'
155+
// // (delete)
156+
// /* note the short passive participle tests:
157+
// '{n}{a}' '{n}' '{n}{o}' '{n}{y}'
158+
// '{e}{n}{a}' '{e}{n}' '{e}{n}{o}' '{e}{n}{y}'
159+
// */
160+
// )
161+
// )
162+
163+
// define noun as (
164+
// [substring] among (
165+
// // '{a}' '{e}{v}' '{o}{v}' '{i}{e}' '{'}{e}' '{e}'
166+
// // '{i}{ia}{m}{i}' '{ia}{m}{i}' '{a}{m}{i}' '{e}{i}' '{i}{i}'
167+
// // '{i}' '{i}{e}{i`}' '{e}{i`}' '{o}{i`}' '{i}{i`}' '{i`}'
168+
// // '{i}{ia}{m}' '{ia}{m}' '{i}{e}{m}' '{e}{m}' '{a}{m}' '{o}{m}'
169+
// // '{o}' '{u}' '{a}{kh}' '{i}{ia}{kh}' '{ia}{kh}' '{y}' '{'}'
170+
// // '{i}{iu}' '{'}{iu}' '{iu}' '{i}{ia}' '{'}{ia}' '{ia}'
171+
// // (delete)
172+
// /* the small class of neuter forms '{e}{n}{i}' '{e}{n}{e}{m}'
173+
// '{e}{n}{a}' '{e}{n}' '{e}{n}{a}{m}' '{e}{n}{a}{m}{i}' '{e}{n}{a}{x}'
174+
// omitted - they only occur on 12 words.
175+
// */
176+
// )
177+
// )
178+
179+
// define derivational as (
180+
// [substring] R2 among (
181+
// // '{o}{s}{t}'
182+
// // '{o}{s}{t}{'}'
183+
// // (delete)
184+
// )
185+
// )
186+
187+
// define tidy_up as (
188+
// [substring] among (
189+
// // '{e}{i`}{sh}'
190+
// // '{e}{i`}{sh}{e}' // superlative forms
191+
// // (delete
192+
// // ['{n}'] '{n}' delete
193+
// // )
194+
// // '{n}'
195+
// // ('{n}' delete) // e.g. -nno endings
196+
// // '{'}'
197+
// // (delete) // with some slight false conflations
198+
// )
199+
// )
200+
)
201+
202+
define stem as (
203+
204+
// Normalise {e"} to {e}. The documentation has long suggested the user
205+
// should do this before calling the stemmer - we now do it for them.
206+
// do repeat ( goto (['{e"}']) <- '{e}' )
207+
208+
do mark_regions
209+
backwards setlimit tomark pV for (
210+
do (
211+
adjectival
212+
// perfective_gerund or
213+
// ( try reflexive
214+
// adjectival or verb or noun
215+
// )
216+
)
217+
// try([ '{i}' ] delete)
218+
// because noun ending -i{iu} is being treated as verb ending -{iu}
219+
220+
// do derivational
221+
// do tidy_up
222+
)
223+
)

tests/algorithms/ukrainian_test.rb

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Words test set
2+
# https://raw.githubusercontent.com/snowballstem/snowball-data/master/russian/voc.txt
3+
4+
# Граматика української мови
5+
# https://uk.wikipedia.org/wiki/%D0%93%D1%80%D0%B0%D0%BC%D0%B0%D1%82%D0%B8%D0%BA%D0%B0_%D1%83%D0%BA%D1%80%D0%B0%D1%97%D0%BD%D1%81%D1%8C%D0%BA%D0%BE%D1%97_%D0%BC%D0%BE%D0%B2%D0%B8
6+
7+
perfective_gerund = { # DONE
8+
'в' => %w[подола|в підня|в],
9+
'вши' => %w[написа|вши підня|вши],
10+
'вшись' => %w[абсолютизува|вшись підня|вшись],
11+
12+
'ивши' => %w[дзвон|ивши],
13+
'ившись' => %w[дзвон|ившись],
14+
'ув' => %w[приб|ув],
15+
'увши' => %w[приб|увши],
16+
'увшись' => %w[дотикн|увшись]
17+
}
18+
19+
adjective = { # DONE
20+
'іше іє і е ими' => %w[зелен|іше послан|іє ароматн|і зіпсован|е зіпсован|ими],
21+
'іший ий ій им' => %w[зелен|іший зелен|ий великодн|ій глибок|им], # noun: ой =>палеоз|ой perfective_gerund: ем => похапц|ем
22+
'ого' => %w[ головн|ого ], # noun ім => екстр|ім ом => заїзд|ом
23+
'ому их у я а' => %w[веснян|ому зелен|их бронзов|у верхн|я весел|а], # !по-весняному?
24+
'ою' => %w[веснян|ою ],
25+
# noun 'ею' => %w[земл|ею],
26+
}
27+
28+
adjectival = {
29+
'ем' => %w[задава|емые изменя|ем],
30+
'нн' => %w[беспреста|нно деревя|нными],
31+
'вш' => %w[отказа|вшись отделя|вший],
32+
'ющ щ' => %w[сталкива|ющихся спеша|щих
33+
умоля|ющими блестя|щему],
34+
35+
'ивш ывш' => %w[брод|ивших несб|ывшееся],
36+
'ующ' => %w[несуществ|ующий]
37+
}
38+
39+
reflexive = {
40+
'ся' => %w[осек|ся],
41+
'сь' => %w[ввы|сь]
42+
}
43+
44+
verb = {
45+
'ла на ете йте ли й' => %w[сдела|ла воспита|на сдела|ете сдела|йте сдела|ли сдела|й
46+
приня|ла потеря|на причиня|ете причиня|йте причиня|ли причиня|й],
47+
48+
'л ем н ло но ет ют' => %w[сдела|л воспита|ем сдела|н сдела|ло сдела|но сдела|ет сдела|ют
49+
приня|л потеря|ем растеря|н причиня|ло настоя|но объясня|ет объясня|ют],
50+
51+
'ны ть ешь' => %w[сдела|ны сдела|ть сдела|ешь
52+
потеря|ны потеря|ть потеря|ешь],
53+
54+
'нно' => %w[пута|нно постоя|нно],
55+
56+
'ила ыла ена ейте' => %w[беспоко|ила прикр|ыла выруч|ена пожал|ейте вспл|ыла], # вспл|ыла?
57+
'уйте ите или ыли ей' => %w[пожал|уйте позвол|ите полюб|или приб|ыли приникш|ей],
58+
'уй ил ыл им ым ен' => %w[протест|уй проход|ил раскр|ыл редк|им решительн|ым свобод|ен],
59+
'ило ыло ено ят ует' => %w[став|ило неун|ыло обознач|ено обрат|ят повеств|ует],
60+
'уют ит ыт ены ить' => %w[преслед|уют прибеж|ит закр|ыт зауч|ены затуш|ить],
61+
'ыть ишь ую ю' => %w[откр|ыть отправ|ишь отперт|ую отрица|ю]
62+
}
63+
64+
noun = {
65+
'а ев ов ие ье е' => %w[вод|а нап|ев вопрос|ов здрав|ие здоров|ье вопрос|е],
66+
'иями ями ами еи ии' => %w[волнен|иями вопл|ями вопрос|ами галер|еи гармон|ии],
67+
'и ией ей ой ий й' => %w[потер|и гармон|ией галере|ей гер|ой ген|ий сара|й],
68+
'иям ям ием ем ам ом' => %w[губерн|иям двер|ям биен|ием братц|ем бумаг|ам букет|ом],
69+
'о у ах иях ях ы ь' => %w[брюх|о брюх|у бумаг|ах здан|иях камн|ях казн|ы камен|ь],
70+
'ию ью ю ия ья я' => %w[комед|ию кров|ью кровл|ю лечен|ия лист|ья локт|я]
71+
}
72+
73+
derivational = {
74+
'ост' => %w[любезн|остей],
75+
'ость' => %w[любезн|остью]
76+
}
77+
78+
tidy_up = {
79+
'н[н]ейш' => %w[наиполезн|ейший смирен|нейший],
80+
'н[н]ейше' => %w[многочислен|нейшее сильн|ейшее],
81+
'н[н]' => %w[смирен|но смирн|о],
82+
'ь' => %w[совест|ь]
83+
}
84+
85+
exceptions = {
86+
# ё => е
87+
'угнетённый' => 'угнетен',
88+
89+
# -ию => ''
90+
'академию' => 'академ'
91+
}
92+
93+
$all_tests = []
94+
$errors = []
95+
96+
def incorrect_stem_msg(result_stem, word, stem)
97+
"Incorrect stemming '#{result_stem}' for word '#{word}', should be '#{stem}'"
98+
end
99+
100+
def check_words_set(words_set)
101+
words_set.each do |_rule, test_cases|
102+
test_cases.each do |test_case|
103+
stem, ending = test_case.split('|')
104+
word = [stem, ending].join
105+
$all_tests << word
106+
result_stem = (`echo "#{word}" | ./stemwords -l ru`).strip
107+
$errors << incorrect_stem_msg(result_stem, word, stem) if result_stem != stem
108+
end
109+
end
110+
end
111+
112+
[
113+
perfective_gerund,
114+
adjective,
115+
# adjectival,
116+
# reflexive,
117+
# verb,
118+
# noun,
119+
# derivational,
120+
# tidy_up
121+
].each {|words_set| check_words_set(words_set) }
122+
123+
# exceptions.each do |word, stem|
124+
# $all_tests << word
125+
# result_stem = (`echo "#{word}" | ./stemwords -l uk`).strip
126+
# $errors << incorrect_stem_msg(result_stem, word, stem) if result_stem != stem
127+
# end
128+
129+
$errors.empty? ? puts("#{$all_tests.count} test(s) passed successfully!") : puts($errors.join("\n"))

0 commit comments

Comments
 (0)