tr8n_core 4.0.1

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 (86) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +69 -0
  4. data/Rakefile +9 -0
  5. data/config/config.yml +34 -0
  6. data/config/tokens/data.yml +45 -0
  7. data/config/tokens/decorations.yml +37 -0
  8. data/lib/tr8n/application.rb +320 -0
  9. data/lib/tr8n/base.rb +123 -0
  10. data/lib/tr8n/cache.rb +144 -0
  11. data/lib/tr8n/cache_adapters/cdb.rb +74 -0
  12. data/lib/tr8n/cache_adapters/file.rb +70 -0
  13. data/lib/tr8n/cache_adapters/memcache.rb +91 -0
  14. data/lib/tr8n/cache_adapters/redis.rb +94 -0
  15. data/lib/tr8n/component.rb +68 -0
  16. data/lib/tr8n/config.rb +291 -0
  17. data/lib/tr8n/decorators/base.rb +35 -0
  18. data/lib/tr8n/decorators/default.rb +30 -0
  19. data/lib/tr8n/decorators/html.rb +63 -0
  20. data/lib/tr8n/exception.rb +26 -0
  21. data/lib/tr8n/language.rb +250 -0
  22. data/lib/tr8n/language_case.rb +116 -0
  23. data/lib/tr8n/language_case_rule.rb +85 -0
  24. data/lib/tr8n/language_context.rb +115 -0
  25. data/lib/tr8n/language_context_rule.rb +62 -0
  26. data/lib/tr8n/logger.rb +83 -0
  27. data/lib/tr8n/rules_engine/evaluator.rb +156 -0
  28. data/lib/tr8n/rules_engine/parser.rb +83 -0
  29. data/lib/tr8n/source.rb +95 -0
  30. data/lib/tr8n/tokens/data.rb +410 -0
  31. data/lib/tr8n/tokens/data_tokenizer.rb +82 -0
  32. data/lib/tr8n/tokens/decoration_tokenizer.rb +200 -0
  33. data/lib/tr8n/tokens/hidden.rb +48 -0
  34. data/lib/tr8n/tokens/method.rb +52 -0
  35. data/lib/tr8n/tokens/transform.rb +191 -0
  36. data/lib/tr8n/translation.rb +104 -0
  37. data/lib/tr8n/translation_key.rb +205 -0
  38. data/lib/tr8n/translator.rb +62 -0
  39. data/lib/tr8n/utils.rb +124 -0
  40. data/lib/tr8n_core/ext/array.rb +74 -0
  41. data/lib/tr8n_core/ext/date.rb +63 -0
  42. data/lib/tr8n_core/ext/fixnum.rb +39 -0
  43. data/lib/tr8n_core/ext/hash.rb +126 -0
  44. data/lib/tr8n_core/ext/string.rb +44 -0
  45. data/lib/tr8n_core/ext/time.rb +71 -0
  46. data/lib/tr8n_core/generators/cache/base.rb +85 -0
  47. data/lib/tr8n_core/generators/cache/cdb.rb +27 -0
  48. data/lib/tr8n_core/generators/cache/file.rb +69 -0
  49. data/lib/tr8n_core/modules/logger.rb +43 -0
  50. data/lib/tr8n_core/version.rb +27 -0
  51. data/lib/tr8n_core.rb +68 -0
  52. data/spec/application_spec.rb +228 -0
  53. data/spec/base_spec.rb +19 -0
  54. data/spec/config_spec.rb +16 -0
  55. data/spec/decorator_spec.rb +10 -0
  56. data/spec/decorators/base_spec.rb +14 -0
  57. data/spec/decorators/default_spec.rb +12 -0
  58. data/spec/decorators/html_spec.rb +50 -0
  59. data/spec/fixtures/application.json +112 -0
  60. data/spec/fixtures/languages/en-US.json +1424 -0
  61. data/spec/fixtures/languages/es.json +291 -0
  62. data/spec/fixtures/languages/ru.json +550 -0
  63. data/spec/fixtures/translations/ru/basic.json +56 -0
  64. data/spec/fixtures/translations/ru/counters.json +43 -0
  65. data/spec/fixtures/translations/ru/genders.json +171 -0
  66. data/spec/fixtures/translations/ru/last_names.txt +200 -0
  67. data/spec/fixtures/translations/ru/names.json +1 -0
  68. data/spec/fixtures/translations/ru/names.txt +458 -0
  69. data/spec/helper.rb +84 -0
  70. data/spec/language_case_rule_spec.rb +57 -0
  71. data/spec/language_case_spec.rb +58 -0
  72. data/spec/language_context_rule_spec.rb +73 -0
  73. data/spec/language_context_spec.rb +331 -0
  74. data/spec/language_spec.rb +16 -0
  75. data/spec/rules_engine/evaluator_spec.rb +148 -0
  76. data/spec/rules_engine/parser_spec.rb +29 -0
  77. data/spec/tokens/data_spec.rb +160 -0
  78. data/spec/tokens/data_tokenizer_spec.rb +29 -0
  79. data/spec/tokens/decoration_tokenizer_spec.rb +81 -0
  80. data/spec/tokens/hidden_spec.rb +24 -0
  81. data/spec/tokens/method_spec.rb +84 -0
  82. data/spec/tokens/transform_spec.rb +50 -0
  83. data/spec/translation_key_spec.rb +96 -0
  84. data/spec/translation_spec.rb +24 -0
  85. data/spec/utils_spec.rb +64 -0
  86. metadata +176 -0
@@ -0,0 +1,458 @@
1
+ Авдотья
2
+
3
+ Аким
4
+
5
+ Аксинья
6
+
7
+ Алевтина
8
+
9
+ Александр
10
+
11
+ Александра
12
+
13
+ Алексей
14
+
15
+ Алена
16
+
17
+ Алина
18
+
19
+ Алла
20
+
21
+ Анастасия
22
+
23
+ Анатолий
24
+
25
+ Ангелина
26
+
27
+ Андрей
28
+
29
+ Анисья
30
+
31
+ Анна
32
+
33
+ Антон
34
+
35
+ Антонина
36
+
37
+ Анфим
38
+
39
+ Анфиса
40
+
41
+ Аполлинария
42
+
43
+ Арина
44
+
45
+ Аркадий
46
+
47
+ Арсений
48
+
49
+ Артем
50
+
51
+ Артемий
52
+
53
+ Богдан
54
+
55
+ Богдана
56
+
57
+ Борис
58
+
59
+ Борислав
60
+
61
+ Вадим
62
+
63
+ Валентин
64
+
65
+ Валентина
66
+
67
+ Валерий
68
+
69
+ Валерия
70
+
71
+ Варвара
72
+
73
+ Василий
74
+
75
+ Василиса
76
+
77
+ Венера
78
+
79
+ Вера
80
+
81
+ Вета
82
+
83
+ Виктор
84
+
85
+ Викторина
86
+
87
+ Виктория
88
+
89
+ Вилена
90
+
91
+ Виолетта
92
+
93
+ Виталий
94
+
95
+ Виталина
96
+ Виталия
97
+
98
+ Влада
99
+
100
+ Владана
101
+
102
+ Владимир
103
+
104
+ Владислав
105
+
106
+ Владислава
107
+
108
+ Владлен
109
+
110
+ Всеволод
111
+
112
+ Вячеслав
113
+
114
+ Галина
115
+
116
+ Геннадий
117
+
118
+ Георгий
119
+
120
+ Герасим
121
+
122
+ Глеб
123
+
124
+ Гордей
125
+
126
+ Григорий
127
+
128
+ Дамир
129
+
130
+ Даниил
131
+
132
+ Данислав
133
+
134
+ Дарья
135
+
136
+ Денис
137
+
138
+ Джереми (Иеремия)
139
+
140
+ Дина
141
+
142
+ Дмитрий
143
+
144
+ Евгений
145
+
146
+ Евгения
147
+
148
+ Евдоким
149
+
150
+ Евдокия
151
+
152
+ Евстахий
153
+
154
+ Егор
155
+
156
+ Екатерина
157
+
158
+ Елена
159
+
160
+ Елизавета
161
+
162
+ Елисей
163
+
164
+ Емельян
165
+
166
+ Еремей
167
+
168
+ Есения
169
+
170
+ Ефим
171
+
172
+ Захар
173
+
174
+ Зинаида
175
+
176
+ Зиновий
177
+
178
+ Злата
179
+
180
+ Зоя
181
+
182
+ Иван
183
+
184
+ Игнат
185
+
186
+ Игнатий
187
+
188
+ Игорь
189
+
190
+ Иероним (Джером)
191
+
192
+ Изабелла
193
+
194
+ Иллирика
195
+
196
+ Илья
197
+
198
+ Инесса
199
+
200
+ Инна
201
+
202
+ Иннокентий
203
+
204
+ Иоанна
205
+
206
+ Ира
207
+
208
+ Ираида
209
+
210
+ Ирина
211
+
212
+ Искра
213
+
214
+ Ия
215
+
216
+ Карина
217
+
218
+ Кира
219
+
220
+ Кирилл
221
+
222
+ Клим
223
+
224
+ Константин
225
+
226
+ Кристина
227
+
228
+ Ксения
229
+
230
+ Кузьма
231
+
232
+ Лада
233
+
234
+ Лара
235
+
236
+ Лариса
237
+
238
+ Лев
239
+
240
+ Леонид
241
+
242
+ Лера
243
+
244
+ Лидия
245
+
246
+ Лика
247
+
248
+ Любовь
249
+
250
+ Людмила
251
+
252
+ Ляля
253
+
254
+ Магдалeна
255
+
256
+ Майя
257
+
258
+ Макар
259
+
260
+ Макарий
261
+
262
+ Макария
263
+
264
+ Максим
265
+
266
+ Маргарита
267
+
268
+ Марина
269
+
270
+ Мария
271
+
272
+ Марк
273
+
274
+ Мартин (Мартын)
275
+
276
+ Марфа
277
+
278
+ Матвей
279
+
280
+ Мила
281
+
282
+ Милада
283
+
284
+ Милана
285
+
286
+ Милена
287
+
288
+ Милица
289
+
290
+ Мира
291
+
292
+ Мирослав
293
+
294
+ Мирослава
295
+
296
+ Мирра
297
+
298
+ Михаил
299
+
300
+ Надежда
301
+
302
+ Наталья
303
+
304
+ Нелли
305
+
306
+ Ника
307
+
308
+ Никита
309
+
310
+ Никодим
311
+
312
+ Никола
313
+
314
+ Николай
315
+
316
+ Нина
317
+
318
+ Нинель
319
+
320
+ Оксана
321
+
322
+ Олег
323
+
324
+ Олеся
325
+
326
+ Ольга
327
+
328
+ Осип (Иосиф)
329
+
330
+ Остап
331
+
332
+ Павел
333
+
334
+ Павлина
335
+
336
+ Пелагея
337
+
338
+ Петр
339
+
340
+ Платон
341
+
342
+ Платонида
343
+
344
+ Полина
345
+
346
+ Прасковья
347
+
348
+ Прохор
349
+
350
+ Рада
351
+
352
+ Радий
353
+
354
+ Радик
355
+
356
+ Радомир
357
+
358
+ Радослав
359
+
360
+ Раиса
361
+
362
+ Рената
363
+
364
+ Римма
365
+
366
+ Ренат
367
+
368
+ Родион
369
+
370
+ Роман
371
+
372
+ Ростислав
373
+
374
+ Русалина
375
+
376
+ Руслан
377
+
378
+ Сабина
379
+
380
+ Савва
381
+
382
+ Савелий
383
+
384
+ Светлана
385
+
386
+ Святослав
387
+
388
+ Севастьян
389
+
390
+ Семен
391
+
392
+ Серафима
393
+
394
+ Сергей
395
+
396
+ Сидор
397
+
398
+ Соня
399
+
400
+ Софья
401
+
402
+ Спартак
403
+
404
+ Станислав
405
+
406
+ Стелла
407
+
408
+ Степан
409
+
410
+ Таисия
411
+
412
+ Тарас
413
+
414
+ Татьяна
415
+
416
+ Тимофей
417
+
418
+ Тихон
419
+
420
+ Трофим
421
+
422
+ Ульяна
423
+
424
+ Фаина
425
+
426
+ Федор
427
+
428
+ Федот
429
+
430
+ Филипп
431
+
432
+ Флор
433
+
434
+ Фома
435
+
436
+ Харитон
437
+
438
+ Цветана
439
+
440
+ Юлиан
441
+
442
+ Юлия
443
+
444
+ Юния
445
+
446
+ Юрий
447
+
448
+ Яков
449
+
450
+ Яна
451
+
452
+ Янина
453
+
454
+ Ярина
455
+
456
+ Ярослав
457
+
458
+ Ярослава
data/spec/helper.rb ADDED
@@ -0,0 +1,84 @@
1
+ require 'rspec'
2
+ require 'json'
3
+
4
+ require 'simplecov'
5
+ require 'coveralls'
6
+
7
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
8
+ SimpleCov::Formatter::HTMLFormatter,
9
+ Coveralls::SimpleCov::Formatter
10
+ ]
11
+ SimpleCov.start
12
+
13
+ require 'tr8n_core'
14
+
15
+ def fixtures_root
16
+ File.join(File.dirname(__FILE__), 'fixtures')
17
+ end
18
+
19
+ def load_json(file_path)
20
+ JSON.parse(File.read("#{fixtures_root}/#{file_path}"))
21
+ end
22
+
23
+ def load_translation_keys_from_file(app, path)
24
+ load_json(path).each do |jkey|
25
+ load_translation_key_from_hash(app, jkey)
26
+ end
27
+ end
28
+
29
+ def load_translation_key_from_hash(app, hash)
30
+ app.cache_translation_key(Tr8n::TranslationKey.new(hash.merge(:application => app)))
31
+ end
32
+
33
+ def stub_object(attrs)
34
+ user = double()
35
+ attrs.each do |key, value|
36
+ user.stub(key) { value }
37
+ end
38
+ user
39
+ end
40
+
41
+ def init_application(locales = [], path = 'application.json')
42
+ locales = ['en-US', 'ru', 'es'] if locales.size == 0
43
+ app = Tr8n::Application.new(load_json(path))
44
+ locales.each do |locale|
45
+ app.add_language(Tr8n::Language.new(load_json("languages/#{locale}.json")))
46
+ end
47
+ Tr8n.config.application = app
48
+ Tr8n.config.current_language = app.language('en-US')
49
+ app
50
+ end
51
+
52
+ RSpec.configure do |config|
53
+ config.before do
54
+ ARGV.replace []
55
+ end
56
+
57
+ config.expect_with :rspec do |c|
58
+ c.syntax = :expect
59
+ end
60
+
61
+ def capture(stream)
62
+ begin
63
+ stream = stream.to_s
64
+ eval "$#{stream} = StringIO.new"
65
+ yield
66
+ result = eval("$#{stream}").string
67
+ ensure
68
+ eval("$#{stream} = #{stream.upcase}")
69
+ end
70
+
71
+ result
72
+ end
73
+
74
+ def source_root
75
+ fixtures_root
76
+ end
77
+
78
+ def destination_root
79
+ File.join(File.dirname(__FILE__), 'sandbox')
80
+ end
81
+
82
+ alias :silence :capture
83
+ end
84
+
@@ -0,0 +1,57 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'helper'
4
+
5
+ describe Tr8n::LanguageCaseRule do
6
+ describe "initialize" do
7
+ it "sets attributes" do
8
+ expect(Tr8n::LanguageCaseRule.attributes).to eq([:language_case, :id, :description, :examples, :conditions, :conditions_expression, :operations, :operations_expression])
9
+ end
10
+ end
11
+
12
+ describe "evaluating simple rules without genders" do
13
+ it "should result in correct substitution" do
14
+ @rule = Tr8n::LanguageCaseRule.new(
15
+ :conditions => "(match '/s$/' @value)",
16
+ :operations => "(append \"'\" @value)"
17
+ )
18
+
19
+ expect(@rule.evaluate("Michael")).to be_false
20
+ expect(@rule.evaluate("Anna")).to be_false
21
+
22
+ expect(@rule.evaluate("friends")).to be_true
23
+ expect(@rule.apply("friends")).to eq("friends'")
24
+
25
+ @rule = Tr8n::LanguageCaseRule.new(
26
+ :conditions => "(not (match '/s$/' @value))",
27
+ :operations => "(append \"'s\" @value)"
28
+ )
29
+
30
+ expect(@rule.evaluate("Michael")).to be_true
31
+ expect(@rule.apply("Michael")).to eq("Michael's")
32
+
33
+ expect(@rule.evaluate("Anna")).to be_true
34
+ expect(@rule.apply("Anna")).to eq("Anna's")
35
+
36
+ expect(@rule.evaluate("friends")).to be_false
37
+
38
+ @rule = Tr8n::LanguageCaseRule.new(
39
+ :conditions => "(= '1' @value))",
40
+ :operations => "(quote 'first')"
41
+ )
42
+
43
+ expect(@rule.evaluate('2')).to be_false
44
+ expect(@rule.evaluate('1')).to be_true
45
+ expect(@rule.apply('1')).to eq("first")
46
+
47
+ @rule = Tr8n::LanguageCaseRule.new(
48
+ :conditions => "(match '/(0|4|5|6|7|8|9|11|12|13)$/' @value))",
49
+ :operations => "(append 'th' @value)"
50
+ )
51
+
52
+ expect(@rule.apply('4')).to eq("4th")
53
+ expect(@rule.apply('15')).to eq("15th")
54
+ end
55
+ end
56
+
57
+ end
@@ -0,0 +1,58 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'helper'
4
+
5
+ describe Tr8n::LanguageCase do
6
+ before do
7
+ @app = init_application
8
+ @english = @app.language('en-US')
9
+ @russian = @app.language('ru')
10
+ end
11
+
12
+ describe "initialize" do
13
+ it "sets attributes" do
14
+ expect(Tr8n::LanguageCase.attributes).to eq([:language, :id, :keyword, :latin_name, :native_name, :description, :application, :rules])
15
+ end
16
+ end
17
+
18
+ describe "apply case" do
19
+ it "should return correct data" do
20
+ lcase = Tr8n::LanguageCase.new(
21
+ :language => @english,
22
+ :keyword => "pos",
23
+ :latin_name => "Possessive",
24
+ :native_name => "Possessive",
25
+ :description => "Used to indicate possession (i.e., ownership). It is usually created by adding 's to the word",
26
+ :application => "phrase"
27
+ )
28
+
29
+ lcase.rules << Tr8n::LanguageCaseRule.new(:conditions => "(match '/s$/' @value)", :operations => "(append \"'\" @value)")
30
+ lcase.rules << Tr8n::LanguageCaseRule.new(:conditions => "(not (match '/s$/' @value))", :operations => "(append \"'s\" @value)")
31
+ expect(lcase.apply("Michael")).to eq("Michael's")
32
+ end
33
+
34
+ it "should correctly process default cases" do
35
+ possessive = @english.language_case_by_keyword('pos')
36
+ expect(possessive.apply("Michael")).to eq("Michael's")
37
+
38
+
39
+ plural = @english.language_case_by_keyword('plural')
40
+
41
+ expect(plural.apply("fish")).to eq("fish")
42
+ expect(plural.apply("money")).to eq("money")
43
+
44
+ # irregular
45
+ expect(plural.apply("move")).to eq("moves")
46
+
47
+ # plurals
48
+ expect(plural.apply("quiz")).to eq("quizzes")
49
+ expect(plural.apply("wife")).to eq("wives")
50
+
51
+ singular = @english.language_case_by_keyword('singular')
52
+ expect(singular.apply("quizzes")).to eq("quiz")
53
+ expect(singular.apply("cars")).to eq("car")
54
+ expect(singular.apply("wives")).to eq("wife")
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,73 @@
1
+ require 'helper'
2
+
3
+ describe Tr8n::LanguageContextRule do
4
+
5
+ describe "initialize" do
6
+ it "sets attributes" do
7
+ expect(Tr8n::LanguageContextRule.attributes).to eq([:language_context, :keyword, :description, :examples, :conditions, :conditions_expression])
8
+ end
9
+ end
10
+
11
+ describe "finding fallback rules" do
12
+ it "should return fallback rule" do
13
+ rule = Tr8n::LanguageContextRule.new(:keyword => "one", :conditions => "(= 1 @n)", :examples => "1")
14
+ expect(rule.fallback?).to be_false
15
+
16
+ rule = Tr8n::LanguageContextRule.new(:keyword => "other", :examples => "0, 2-999; 1.2, 2.07...")
17
+ expect(rule.fallback?).to be_true
18
+ end
19
+ end
20
+
21
+ describe "evaluating rules" do
22
+ it "should return correct results" do
23
+ rule = Tr8n::LanguageContextRule.new(:keyword => "one", :conditions => "(= 1 @n)", :examples => "1")
24
+ expect(rule.evaluate).to be_false
25
+ expect(rule.evaluate({"@n" => 1})).to be_true
26
+ expect(rule.evaluate({"@n" => 2})).to be_false
27
+ expect(rule.evaluate({"@n" => 0})).to be_false
28
+
29
+ one = Tr8n::LanguageContextRule.new(:keyword => "one", :conditions => "(&& (= 1 (mod @n 10)) (!= 11 (mod @n 100)))", :description => "{n} mod 10 is 1 and {n} mod 100 is not 11", :examples => "1, 21, 31, 41, 51, 61...")
30
+ few = Tr8n::LanguageContextRule.new(:keyword => "few", :conditions => "(&& (in '2..4' (mod @n 10)) (not (in '12..14' (mod @n 100))))", :description => "{n} mod 10 in 2..4 and {n} mod 100 not in 12..14", :examples => "2-4, 22-24, 32-34...")
31
+ many = Tr8n::LanguageContextRule.new(:keyword => "many", :conditions => "(|| (= 0 (mod @n 10)) (in '5..9' (mod @n 10)) (in '11..14' (mod @n 100)))", :description => "{n} mod 10 is 0 or {n} mod 10 in 5..9 or {n} mod 100 in 11..14", :examples => "0, 5-20, 25-30, 35-40...")
32
+
33
+ {
34
+ [1, 21, 31, 101, 121] => one,
35
+ [2, 3, 4, 22, 23, 24, 102, 103, 104] => few,
36
+ [0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17, 20, 25, 26, 28, 30, 35, 36, 38, 39, 40] => many
37
+ }.each do |vals, rule|
38
+ vals.each do |val|
39
+ vars = {"@n" => val}
40
+ expect(rule.evaluate(vars)).to be_true
41
+ end
42
+ end
43
+
44
+ {
45
+ [2, 3, 4, 9] => one,
46
+ [5, 6, 7, 8, 9] => few,
47
+ [1, 2, 3, 4] => many
48
+ }.each do |vals, rule|
49
+ vals.each do |val|
50
+ vars = {"@n" => val}
51
+ expect(rule.evaluate(vars)).to be_false
52
+ end
53
+ end
54
+
55
+ one_male = Tr8n::LanguageContextRule.new(:keyword => "one_male", :conditions => "(&& (= 1 (count @genders)) (all @genders 'male'))", :description => "List contains one male user")
56
+ one_female = Tr8n::LanguageContextRule.new(:keyword => "one_female", :conditions => "(&& (= 1 (count @genders)) (all @genders 'female'))", :description => "List contains one female user")
57
+ one_unknown = Tr8n::LanguageContextRule.new(:keyword => "one_unknown", :conditions => "(&& (= 1 (count @genders)) (all @genders 'unknown'))", :description => "List contains one user with unknown gender")
58
+ many = Tr8n::LanguageContextRule.new(:keyword => "many", :conditions => "(> (count @genders) 1)", :description => "List contains two or more users")
59
+
60
+ expect(one_male.evaluate({"@genders" => ["male"]})).to be_true
61
+ expect(one_male.evaluate({"@genders" => ["male", "male"]})).to be_false
62
+
63
+ expect(one_female.evaluate({"@genders" => ["female"]})).to be_true
64
+ expect(many.evaluate({"@genders" => ["female", "male"]})).to be_true
65
+
66
+ expect(one_unknown.evaluate({"@genders" => ["unknown"]})).to be_true
67
+
68
+ many_male = Tr8n::LanguageContextRule.new(:keyword => "one", :conditions => "(&& (> (count @genders) 1) (all @genders 'male'))", :description => "List contains at least two users, all male")
69
+ expect(many_male.evaluate({"@genders" => ["male", "male"]})).to be_true
70
+ end
71
+ end
72
+
73
+ end