cldr-plurals 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +18 -0
- data/History.txt +3 -0
- data/README.md +139 -0
- data/Rakefile +84 -0
- data/cldr-plurals.gemspec +18 -0
- data/lib/cldr-plurals.rb +7 -0
- data/lib/cldr-plurals/compiler.rb +43 -0
- data/lib/cldr-plurals/compiler/emitter.rb +30 -0
- data/lib/cldr-plurals/compiler/nodes.rb +86 -0
- data/lib/cldr-plurals/compiler/parser.rb +153 -0
- data/lib/cldr-plurals/compiler/tokenizer.rb +56 -0
- data/lib/cldr-plurals/javascript_emitter.rb +106 -0
- data/lib/cldr-plurals/ruby_emitter.rb +108 -0
- data/lib/cldr-plurals/version.rb +5 -0
- data/spec/javascript_sample_spec.rb +48 -0
- data/spec/ruby_sample_spec.rb +43 -0
- data/spec/samples.yml +3371 -0
- data/spec/spec_helper.rb +49 -0
- metadata +63 -0
@@ -0,0 +1,153 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'cldr-plurals/compiler/nodes'
|
4
|
+
|
5
|
+
module CldrPlurals
|
6
|
+
module Compiler
|
7
|
+
class Parser
|
8
|
+
|
9
|
+
OPERATORS = [
|
10
|
+
:modulo, :equals, :not_equals
|
11
|
+
]
|
12
|
+
|
13
|
+
SAMPLES = [
|
14
|
+
:int_sample, :dec_sample
|
15
|
+
]
|
16
|
+
|
17
|
+
class UnexpectedTokenError < StandardError; end
|
18
|
+
|
19
|
+
attr_reader :token_list, :stack, :counter
|
20
|
+
|
21
|
+
def initialize(token_list)
|
22
|
+
@token_list = token_list
|
23
|
+
@stack = []
|
24
|
+
@counter = 0
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse
|
28
|
+
until eol? || SAMPLES.include?(current_token.type) do
|
29
|
+
condition
|
30
|
+
end
|
31
|
+
|
32
|
+
Rule.new(stack.pop)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# and/or
|
38
|
+
def condition
|
39
|
+
and_condition
|
40
|
+
|
41
|
+
while consume(:or)
|
42
|
+
and_condition
|
43
|
+
stack.push(
|
44
|
+
OrCondition.new(stack.pop, stack.pop)
|
45
|
+
)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def and_condition
|
50
|
+
relation
|
51
|
+
|
52
|
+
while consume(:and)
|
53
|
+
relation
|
54
|
+
stack.push(
|
55
|
+
AndCondition.new(stack.pop, stack.pop)
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def relation
|
61
|
+
expr = expression
|
62
|
+
|
63
|
+
stack.push(
|
64
|
+
if OPERATORS.include?(current_token.type)
|
65
|
+
Relation.new(expr, operator, value_set)
|
66
|
+
else
|
67
|
+
expr
|
68
|
+
end
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def expression
|
73
|
+
Expression.new(operand, operator, value_set)
|
74
|
+
end
|
75
|
+
|
76
|
+
def operand
|
77
|
+
token = current_token
|
78
|
+
consume!(:operand)
|
79
|
+
Operand.new(token.value)
|
80
|
+
end
|
81
|
+
|
82
|
+
def operator
|
83
|
+
token = current_token
|
84
|
+
consume!(*OPERATORS)
|
85
|
+
Operator.new(token.value)
|
86
|
+
end
|
87
|
+
|
88
|
+
def value_set
|
89
|
+
values = [value]
|
90
|
+
|
91
|
+
while current_token.type == :comma
|
92
|
+
consume!(:comma)
|
93
|
+
values << value
|
94
|
+
end
|
95
|
+
|
96
|
+
if values.size == 1
|
97
|
+
values.first
|
98
|
+
else
|
99
|
+
ValueSet.new(values)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def value
|
104
|
+
first_token = current_token
|
105
|
+
consume!(:number)
|
106
|
+
|
107
|
+
case current_token.type
|
108
|
+
when :range
|
109
|
+
consume!(:range)
|
110
|
+
second_token = current_token
|
111
|
+
consume!(:number)
|
112
|
+
Range.new(first_token.value, second_token.value)
|
113
|
+
else
|
114
|
+
first_token.value
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def next_token
|
119
|
+
@counter += 1
|
120
|
+
current_token
|
121
|
+
end
|
122
|
+
|
123
|
+
def current_token
|
124
|
+
if eol?
|
125
|
+
@eol_token ||= Token.new(nil, :eol)
|
126
|
+
else
|
127
|
+
token_list[counter]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def consume!(*token_types)
|
132
|
+
unless consume(*token_types)
|
133
|
+
raise UnexpectedTokenError,
|
134
|
+
"expected '#{token_types.join(', ')}', got '#{current_token.type}'"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def consume(*token_types)
|
139
|
+
if token_types.include?(current_token.type)
|
140
|
+
next_token
|
141
|
+
true
|
142
|
+
else
|
143
|
+
false
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def eol?
|
148
|
+
counter >= token_list.size
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module CldrPlurals
|
4
|
+
module Compiler
|
5
|
+
class Token
|
6
|
+
attr_reader :value, :type
|
7
|
+
|
8
|
+
def initialize(value, type)
|
9
|
+
@value = value
|
10
|
+
@type = type
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Tokenizer
|
15
|
+
|
16
|
+
TOKENS = {
|
17
|
+
/@integer/ => :int_sample,
|
18
|
+
/@decimal/ => :dec_sample,
|
19
|
+
/\u2026/ => :infinite_set,
|
20
|
+
/~/ => :sample_range,
|
21
|
+
/and/ => :and,
|
22
|
+
/or/ => :or,
|
23
|
+
/[niftvw]/ => :operand,
|
24
|
+
/,/ => :comma,
|
25
|
+
/\.\./ => :range,
|
26
|
+
/%/ => :modulo,
|
27
|
+
/=/ => :equals,
|
28
|
+
/\!=/ => :not_equals,
|
29
|
+
/[\d]+/ => :number
|
30
|
+
}
|
31
|
+
|
32
|
+
ALL_TOKENS = Regexp.compile(
|
33
|
+
TOKENS.map { |r, _| r.source }.join('|')
|
34
|
+
)
|
35
|
+
|
36
|
+
def self.tokenize(text)
|
37
|
+
text.scan(ALL_TOKENS).each_with_object([]) do |token, ret|
|
38
|
+
found_type = TOKENS.each_pair do |regex, token_type|
|
39
|
+
break token_type if token =~ regex
|
40
|
+
end
|
41
|
+
|
42
|
+
if found_type.is_a?(Symbol)
|
43
|
+
ret << make_token(token, found_type)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def self.make_token(value, type)
|
51
|
+
Token.new(value, type)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module CldrPlurals
|
4
|
+
class JavascriptEmitter < Compiler::Emitter
|
5
|
+
class << self
|
6
|
+
|
7
|
+
RUNTIME_VARS = %w(n i v w f t)
|
8
|
+
|
9
|
+
def emit_rules(rule_list)
|
10
|
+
parts = rule_list.rules.map do |rule|
|
11
|
+
"(#{emit_rule(rule)} ? '#{rule.name}' : "
|
12
|
+
end + ["'other'"]
|
13
|
+
|
14
|
+
chooser = "#{parts.join('')}#{')' * (parts.size - 1)}"
|
15
|
+
"(function(num, runtime) { #{build_runtime_vars}; return #{chooser}; })"
|
16
|
+
end
|
17
|
+
|
18
|
+
def emit_rule(rule)
|
19
|
+
emit(rule.root)
|
20
|
+
end
|
21
|
+
|
22
|
+
def emit_rule_standalone(rule)
|
23
|
+
"(function(n, i, f, t, v, w) { return #{emit_rule(rule)}; })"
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def build_runtime_vars
|
29
|
+
RUNTIME_VARS.map do |var|
|
30
|
+
"var #{var} = runtime.#{var}(num)"
|
31
|
+
end.join('; ')
|
32
|
+
end
|
33
|
+
|
34
|
+
def emit_or_condition(cond)
|
35
|
+
"(#{emit(cond.left)} || #{emit(cond.right)})"
|
36
|
+
end
|
37
|
+
|
38
|
+
def emit_and_condition(cond)
|
39
|
+
"(#{emit(cond.left)} && #{emit(cond.right)})"
|
40
|
+
end
|
41
|
+
|
42
|
+
def emit_expression(expr)
|
43
|
+
case expr.value
|
44
|
+
when CldrPlurals::Compiler::Range
|
45
|
+
neg = expr.operation.symbol == '!=' ? '!' : ''
|
46
|
+
operand_str = emit(expr.operand)
|
47
|
+
"#{neg}((#{operand_str} >= #{expr.value.start}) && (#{operand_str} <= #{expr.value.finish}))"
|
48
|
+
when CldrPlurals::Compiler::ValueSet
|
49
|
+
"(#{emit_value_set(expr.value, expr.operand, expr.operation)})"
|
50
|
+
else
|
51
|
+
emit_all(expr.operand, expr.operation, expr.value).join(' ')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def emit_relation(rel)
|
56
|
+
case rel.value
|
57
|
+
when CldrPlurals::Compiler::Range
|
58
|
+
expr = emit(rel.expression)
|
59
|
+
neg = rel.operation.symbol == '!=' ? '!' : ''
|
60
|
+
"#{neg}((#{expr} >= #{rel.value.start}) && (#{expr} <= #{rel.value.finish}))"
|
61
|
+
when CldrPlurals::Compiler::ValueSet
|
62
|
+
"(#{emit_value_set(rel.value, rel.expression, rel.operation)})"
|
63
|
+
else
|
64
|
+
emit_all(rel.expression, rel.operation, rel.value).join(' ')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def emit_value_set(value_set, operand, operator)
|
69
|
+
values = value_set.values.map do |value|
|
70
|
+
case value
|
71
|
+
when CldrPlurals::Compiler::Range
|
72
|
+
neg = operator.symbol == '!=' ? '!' : ''
|
73
|
+
operand_str = emit(operand)
|
74
|
+
"#{neg}((#{operand_str} >= #{value.start}) && (#{operand_str} <= #{value.finish}))"
|
75
|
+
else
|
76
|
+
"(#{emit(operand)} #{emit(operator)} #{emit(value)})"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
if operator.symbol == '!='
|
81
|
+
values.join(' && ')
|
82
|
+
else
|
83
|
+
values.join(' || ')
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def emit_operator(op)
|
88
|
+
case op.symbol
|
89
|
+
when '='
|
90
|
+
'=='
|
91
|
+
else
|
92
|
+
op.symbol
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def emit_operand(op)
|
97
|
+
op.symbol
|
98
|
+
end
|
99
|
+
|
100
|
+
def emit_string(str)
|
101
|
+
str
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module CldrPlurals
|
4
|
+
class RubyEmitter < Compiler::Emitter
|
5
|
+
class << self
|
6
|
+
|
7
|
+
RUNTIME_VARS = %w(n i v w f t)
|
8
|
+
|
9
|
+
def emit_rules(rule_list)
|
10
|
+
parts = rule_list.rules.map do |rule|
|
11
|
+
"(#{emit_rule(rule)} ? :#{rule.name} : "
|
12
|
+
end + [':other']
|
13
|
+
|
14
|
+
chooser = "#{parts.join('')}#{')' * (parts.size - 1)}"
|
15
|
+
"lambda { |num, runtime| #{build_runtime_vars}; #{chooser} }"
|
16
|
+
end
|
17
|
+
|
18
|
+
def emit_rule(rule)
|
19
|
+
emit(rule.root)
|
20
|
+
end
|
21
|
+
|
22
|
+
def emit_rule_standalone(rule)
|
23
|
+
"lambda { |n, i, f, t, v, w| #{emit_rule(rule)} }"
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def build_runtime_vars
|
29
|
+
RUNTIME_VARS.map do |var|
|
30
|
+
"#{var} = runtime.#{var}(num)"
|
31
|
+
end.join('; ')
|
32
|
+
end
|
33
|
+
|
34
|
+
def emit_or_condition(cond)
|
35
|
+
"(#{emit(cond.left)} || #{emit(cond.right)})"
|
36
|
+
end
|
37
|
+
|
38
|
+
def emit_and_condition(cond)
|
39
|
+
"(#{emit(cond.left)} && #{emit(cond.right)})"
|
40
|
+
end
|
41
|
+
|
42
|
+
def emit_expression(expr)
|
43
|
+
case expr.value
|
44
|
+
when CldrPlurals::Compiler::Range
|
45
|
+
neg = expr.operation.symbol == '!=' ? '!' : ''
|
46
|
+
"#{neg}(#{emit_range(expr.value)}).include?(#{emit(expr.operand)})"
|
47
|
+
when CldrPlurals::Compiler::ValueSet
|
48
|
+
"(#{emit_value_set(expr.value, expr.operand, expr.operation)})"
|
49
|
+
else
|
50
|
+
emit_all(expr.operand, expr.operation, expr.value).join(' ')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def emit_relation(rel)
|
55
|
+
case rel.value
|
56
|
+
when CldrPlurals::Compiler::Range
|
57
|
+
expr = emit(rel.expression)
|
58
|
+
neg = rel.operation.symbol == '!=' ? '!' : ''
|
59
|
+
"#{neg}(#{emit_range(rel.value)}).include?(#{expr})"
|
60
|
+
when CldrPlurals::Compiler::ValueSet
|
61
|
+
"(#{emit_value_set(rel.value, rel.expression, rel.operation)})"
|
62
|
+
else
|
63
|
+
emit_all(rel.expression, rel.operation, rel.value).join(' ')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def emit_value_set(value_set, operand, operator)
|
68
|
+
values = value_set.values.map do |value|
|
69
|
+
case value
|
70
|
+
when CldrPlurals::Compiler::Range
|
71
|
+
neg = operator.symbol == '!=' ? '!' : ''
|
72
|
+
"#{neg}(#{emit_range(value)}).include?(#{emit(operand)})"
|
73
|
+
else
|
74
|
+
"(#{emit(operand)} #{emit(operator)} #{emit(value)})"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
if operator.symbol == '!='
|
79
|
+
values.join(' && ')
|
80
|
+
else
|
81
|
+
values.join(' || ')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def emit_range(range)
|
86
|
+
"#{emit(range.start)}..#{emit(range.finish)}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def emit_operator(op)
|
90
|
+
case op.symbol
|
91
|
+
when '='
|
92
|
+
'=='
|
93
|
+
else
|
94
|
+
op.symbol
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def emit_operand(op)
|
99
|
+
op.symbol
|
100
|
+
end
|
101
|
+
|
102
|
+
def emit_string(str)
|
103
|
+
str
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'v8'
|
4
|
+
require 'yaml'
|
5
|
+
require 'spec_helper'
|
6
|
+
require 'cldr-plurals/javascript_runtime'
|
7
|
+
|
8
|
+
include CldrPlurals
|
9
|
+
include CldrPlurals::Compiler
|
10
|
+
|
11
|
+
describe 'ruby rules' do
|
12
|
+
each_rule do |locales, rule, samples|
|
13
|
+
context = V8::Context.new
|
14
|
+
js_code = JavascriptEmitter.emit_rule_standalone(rule)
|
15
|
+
rule_obj = context.eval(js_code)
|
16
|
+
runtime = context.eval(JavascriptRuntime.source)
|
17
|
+
|
18
|
+
samples.each do |sample_info|
|
19
|
+
context "#{sample_info[:type]} samples" do
|
20
|
+
sample_info[:samples].each do |sample|
|
21
|
+
it sample do
|
22
|
+
args = runtime.buildArgsFor(sample)
|
23
|
+
expect(rule_obj.call(*args)).to eq(true)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'ruby rule lists' do
|
32
|
+
each_rule_list do |rule_list, samples_per_name|
|
33
|
+
context rule_list.locale do
|
34
|
+
context = V8::Context.new
|
35
|
+
js_code = rule_list.to_code(:javascript)
|
36
|
+
rule_proc = context.eval(js_code)
|
37
|
+
runtime = context.eval(JavascriptRuntime.source)
|
38
|
+
|
39
|
+
samples_per_name.each_pair do |name, samples|
|
40
|
+
samples.each do |sample|
|
41
|
+
it "#{name} #{sample}" do
|
42
|
+
expect(rule_proc.call(sample, runtime)).to eq(name.to_s)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|