csl 1.0.0.pre5 → 1.0.0.pre6
Sign up to get free protection for your applications and to get access to all the features.
- data/csl.gemspec +3 -0
- data/features/locales/ordinalize.feature +288 -399
- data/lib/csl.rb +3 -0
- data/lib/csl/info.rb +31 -18
- data/lib/csl/locale.rb +64 -168
- data/lib/csl/locale/ordinalize.rb +130 -0
- data/lib/csl/locale/term.rb +67 -30
- data/lib/csl/node.rb +44 -2
- data/lib/csl/schema.rb +4 -0
- data/lib/csl/style/number.rb +2 -1
- data/lib/csl/style/text.rb +6 -4
- data/lib/csl/version.rb +1 -1
- data/spec/csl/info_spec.rb +7 -1
- data/spec/csl/locale/term_spec.rb +1 -1
- data/spec/csl/node_spec.rb +40 -0
- metadata +23 -8
@@ -0,0 +1,130 @@
|
|
1
|
+
module CSL
|
2
|
+
class Locale
|
3
|
+
|
4
|
+
# Ordinalizes the passed-in number using either the ordinal or
|
5
|
+
# long-ordinal forms defined by the locale. If a long-ordinal form is
|
6
|
+
# requested but not available, the regular ordinal will be returned
|
7
|
+
# instead.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# Locale.load('en').ordinalize(13)
|
11
|
+
# #-> "13th"
|
12
|
+
#
|
13
|
+
# de = Locale.load('de')
|
14
|
+
# de.ordinalize(13)
|
15
|
+
# #-> "13."
|
16
|
+
#
|
17
|
+
# de.ordinalize(3, :form => :long, :gender => :feminine)
|
18
|
+
# #-> "dritte"
|
19
|
+
#
|
20
|
+
# @note
|
21
|
+
# For CSL 1.0 (and older) locales that do not define an "ordinal-00"
|
22
|
+
# term the algorithm specified by CSL 1.0 is used; otherwise uses the
|
23
|
+
# CSL 1.0.1 algorithm with improved support for languages other than
|
24
|
+
# English.
|
25
|
+
#
|
26
|
+
# @param number [#to_i] the number to ordinalize
|
27
|
+
# @param options [Hash] formatting options
|
28
|
+
#
|
29
|
+
# @option options [:short,:long] :form (:short) which ordinals form to use
|
30
|
+
# @option options [:feminine,:masculine,:neutral] :gender (:neutral)
|
31
|
+
# which ordinals gender-form to use
|
32
|
+
#
|
33
|
+
# @raise [ArgumentError] if number cannot be converted to an integer
|
34
|
+
#
|
35
|
+
# @return [String] the ordinal for the passed-in number
|
36
|
+
def ordinalize(number, options = {})
|
37
|
+
raise ArgumentError, "unable to ordinalize #{number}; integer expected" unless
|
38
|
+
number.respond_to?(:to_i)
|
39
|
+
|
40
|
+
number, query = number.to_i, ordinalize_query_for(options)
|
41
|
+
|
42
|
+
key = query[:name]
|
43
|
+
|
44
|
+
# Try to match long-ordinals first
|
45
|
+
if key.start_with?('l')
|
46
|
+
query[:name] = key % number.abs
|
47
|
+
ordinal = terms[query]
|
48
|
+
|
49
|
+
if ordinal.nil?
|
50
|
+
key = 'ordinal-%02d'
|
51
|
+
else
|
52
|
+
return ordinal.to_s(options)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# CSL 1.0 (legacy algorithm)
|
57
|
+
return legacy_ordinalize(number) if legacy?
|
58
|
+
|
59
|
+
#
|
60
|
+
# CSL 1.0.1
|
61
|
+
#
|
62
|
+
|
63
|
+
# Calculate initial modulus
|
64
|
+
mod = 10 ** Math.log10([number.abs, 1].max).to_i
|
65
|
+
|
66
|
+
# Try to find direct match first
|
67
|
+
query.merge! :name => key % number.abs
|
68
|
+
ordinal = terms[query]
|
69
|
+
|
70
|
+
# Try to match modulus of number, dividing mod by 10 at each
|
71
|
+
# iteration until a match is found
|
72
|
+
while ordinal.nil? && mod > 1
|
73
|
+
query.merge! :name => key % (number.abs % mod)
|
74
|
+
ordinal = terms.lookup_modulo(query, mod)
|
75
|
+
|
76
|
+
mod = mod / 10
|
77
|
+
end
|
78
|
+
|
79
|
+
# If we have not found a match at this point, we try to match
|
80
|
+
# the default ordinal instead
|
81
|
+
if ordinal.nil?
|
82
|
+
query[:name] = 'ordinal'
|
83
|
+
ordinal = terms[query]
|
84
|
+
|
85
|
+
if ordinal.nil? && query.key?(:'gender-form')
|
86
|
+
query.delete(:'gender-form')
|
87
|
+
ordinal = terms[query]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
if ordinal.nil?
|
92
|
+
number.to_s
|
93
|
+
else
|
94
|
+
[number, ordinal.to_s(options)].join
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# @return [Hash] a valid ordinalize query; the name attribute is a format string
|
101
|
+
def ordinalize_query_for(options)
|
102
|
+
q = { :name => 'ordinal-%02d' }
|
103
|
+
|
104
|
+
unless options.nil?
|
105
|
+
if options.key?(:form) && options[:form].to_s =~ /^long(-ordinal)?$/i
|
106
|
+
q[:name] = 'long-ordinal-%02d'
|
107
|
+
end
|
108
|
+
|
109
|
+
gender = (options[:'gender-form'] || options[:gender]).to_s
|
110
|
+
unless gender.empty? || gender =~ /^n/i
|
111
|
+
q[:'gender-form'] = (gender =~ /^m/i) ? 'masculine' : 'feminine'
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
q
|
116
|
+
end
|
117
|
+
|
118
|
+
def legacy_ordinalize(number)
|
119
|
+
case
|
120
|
+
when (11..13).include?(number.abs % 100)
|
121
|
+
[number, terms['ordinal-04']].join
|
122
|
+
when (1..3).include?(number.abs % 10)
|
123
|
+
[number, terms['ordinal-%02d' % (number.abs % 10)]].join
|
124
|
+
else
|
125
|
+
[number, terms['ordinal-04']].join
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
data/lib/csl/locale/term.rb
CHANGED
@@ -1,91 +1,128 @@
|
|
1
1
|
module CSL
|
2
2
|
class Locale
|
3
|
-
|
3
|
+
|
4
4
|
class Terms < Node
|
5
5
|
attr_children :term
|
6
|
-
|
6
|
+
|
7
7
|
alias terms term
|
8
8
|
def_delegators :terms, :size, :length
|
9
|
-
|
9
|
+
|
10
10
|
undef_method :[]=
|
11
|
-
|
11
|
+
|
12
12
|
def initialize(attributes = {})
|
13
|
-
super(attributes)
|
13
|
+
super(attributes)
|
14
14
|
@registry, children[:term] = Hash.new { |h,k| h[k] = [] }, []
|
15
15
|
|
16
16
|
yield self if block_given?
|
17
17
|
end
|
18
18
|
|
19
19
|
alias each each_child
|
20
|
-
|
20
|
+
|
21
21
|
def lookup(query)
|
22
22
|
query = { :name => query } unless query.is_a?(Hash)
|
23
|
-
|
23
|
+
|
24
24
|
terms = if query[:name].is_a?(Regexp)
|
25
25
|
registry.select { |name, _| name =~ query[:name] }.flatten(1)
|
26
26
|
else
|
27
27
|
registry[query[:name].to_s]
|
28
28
|
end
|
29
29
|
|
30
|
-
terms.detect { |t| t.
|
30
|
+
terms.detect { |t| t.match?(query) }
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
alias [] lookup
|
34
|
+
|
35
|
+
def lookup_modulo(query, divisor)
|
36
|
+
term = lookup(query)
|
37
|
+
return if term.nil? || !term.match_modulo?(divisor)
|
38
|
+
term
|
39
|
+
end
|
34
40
|
|
35
41
|
private
|
36
|
-
|
42
|
+
|
37
43
|
# @!attribute [r] registry
|
38
44
|
# @return [Hash] a private registry to map term names to the respective
|
39
45
|
# term objects for quick term look-up
|
40
46
|
attr_reader :registry
|
41
|
-
|
47
|
+
|
42
48
|
def added_child(term)
|
43
49
|
raise ValidationError, "failed to register term #{term.inspect}: name attribute missing" unless
|
44
50
|
term.attribute?(:name)
|
45
|
-
|
51
|
+
|
46
52
|
registry[term[:name]].push(term)
|
47
53
|
term
|
48
54
|
end
|
49
|
-
|
55
|
+
|
50
56
|
def deleted_child(term)
|
51
57
|
registry[term[:name]].delete(term)
|
52
58
|
end
|
53
59
|
end
|
54
|
-
|
60
|
+
|
55
61
|
class Term < Node
|
56
|
-
attr_struct :name, :form, :gender, :'gender-form'
|
62
|
+
attr_struct :name, :form, :gender, :'gender-form', :match
|
57
63
|
attr_children :single, :multiple
|
58
64
|
|
59
65
|
attr_accessor :text
|
60
66
|
|
61
67
|
def_delegators :attributes, :hash, :eql?, :name, :form, :gender
|
62
68
|
|
69
|
+
# This method returns whether or not the ordinal term matchs the
|
70
|
+
# passed-in modulus. This is determined by the ordinal term's match
|
71
|
+
# attribute: a value of '2-digits' matches a divisor of 100, '1-digit'
|
72
|
+
# matches a divisor of 10 and 'whole-number' matches a divisor of 1.
|
73
|
+
#
|
74
|
+
# If the term is no ordinal term, this methods always returns false.
|
75
|
+
#
|
76
|
+
# @return [Boolean] whether or not the ordinal term matches the
|
77
|
+
# passed-in divisor.
|
78
|
+
def match_modulo?(divisor)
|
79
|
+
return false unless ordinal?
|
80
|
+
|
81
|
+
case attributes.match
|
82
|
+
when '2-digits'
|
83
|
+
divisor.to_i == 100
|
84
|
+
when '1-digit'
|
85
|
+
divisor.to_i == 10
|
86
|
+
when 'whole-number'
|
87
|
+
divisor.to_i == 1
|
88
|
+
else
|
89
|
+
true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
alias matches_modulo? match_modulo?
|
94
|
+
|
95
|
+
# @return [Boolean] whether or not this term is an ordinal term
|
96
|
+
def ordinal?
|
97
|
+
/^ordinal(-\d\d+)?$/ === attributes.name
|
98
|
+
end
|
99
|
+
|
63
100
|
def gendered?
|
64
101
|
!attributes.gender.blank?
|
65
102
|
end
|
66
|
-
|
103
|
+
|
67
104
|
def neutral?
|
68
105
|
!gendered?
|
69
106
|
end
|
70
|
-
|
107
|
+
|
71
108
|
def textnode?
|
72
109
|
!text.blank?
|
73
110
|
end
|
74
|
-
|
111
|
+
|
75
112
|
def singularize
|
76
113
|
return text if textnode?
|
77
114
|
children.single.to_s
|
78
115
|
end
|
79
116
|
|
80
117
|
alias singular singularize
|
81
|
-
|
118
|
+
|
82
119
|
def pluralize
|
83
120
|
return text if textnode?
|
84
121
|
children.multiple.to_s
|
85
|
-
end
|
122
|
+
end
|
123
|
+
|
124
|
+
alias plural pluralize
|
86
125
|
|
87
|
-
alias plural pluralize
|
88
|
-
|
89
126
|
# @!method masculine?
|
90
127
|
# @return [Boolean] whether or not the term is masculine
|
91
128
|
|
@@ -103,7 +140,7 @@ module CSL
|
|
103
140
|
define_method("#{name}?") do
|
104
141
|
attributes.gender.to_s == name
|
105
142
|
end
|
106
|
-
|
143
|
+
|
107
144
|
define_method("#{name}!") do
|
108
145
|
return nil if attributes.gender.to_s == name
|
109
146
|
attributes.gender = name
|
@@ -139,21 +176,21 @@ module CSL
|
|
139
176
|
end
|
140
177
|
end
|
141
178
|
end
|
142
|
-
|
179
|
+
|
143
180
|
class Single < TextNode; end
|
144
181
|
class Multiple < TextNode; end
|
145
|
-
|
182
|
+
|
146
183
|
private
|
147
|
-
|
184
|
+
|
148
185
|
def pluralize?(options)
|
149
186
|
return false if options.nil?
|
150
|
-
|
187
|
+
|
151
188
|
case
|
152
189
|
when options.key?(:plural) || options.key?('plural')
|
153
190
|
options[:plural] || options['plural']
|
154
191
|
when options.key?(:number) || options.key?('number')
|
155
192
|
key = options[:number] || options['number']
|
156
|
-
|
193
|
+
|
157
194
|
if key.is_a?(Fixnum) || key.to_s =~ /^[+-]?\d+$/
|
158
195
|
key.to_i > 1
|
159
196
|
else
|
@@ -163,9 +200,9 @@ module CSL
|
|
163
200
|
false
|
164
201
|
end
|
165
202
|
end
|
166
|
-
|
203
|
+
|
167
204
|
end
|
168
|
-
|
205
|
+
|
169
206
|
TextNode.types << Term
|
170
207
|
end
|
171
208
|
end
|
data/lib/csl/node.rb
CHANGED
@@ -68,6 +68,28 @@ module CSL
|
|
68
68
|
|
69
69
|
private
|
70
70
|
|
71
|
+
def has_language
|
72
|
+
attr_accessor :language
|
73
|
+
|
74
|
+
define_method :has_language? do
|
75
|
+
!language.nil?
|
76
|
+
end
|
77
|
+
|
78
|
+
public :language, :language=, :has_language?
|
79
|
+
|
80
|
+
alias_method :original_attribute_assignments, :attribute_assignments
|
81
|
+
|
82
|
+
define_method :attribute_assignments do
|
83
|
+
if has_language?
|
84
|
+
original_attribute_assignments.unshift('xml:lang="%s"' % language)
|
85
|
+
else
|
86
|
+
original_attribute_assignments
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private :original_attribute_assignments, :attribute_assignments
|
91
|
+
end
|
92
|
+
|
71
93
|
def attr_defaults(attributes)
|
72
94
|
@default_attributes = attributes
|
73
95
|
end
|
@@ -193,6 +215,10 @@ module CSL
|
|
193
215
|
!attributes.empty?
|
194
216
|
end
|
195
217
|
|
218
|
+
def has_language?
|
219
|
+
false
|
220
|
+
end
|
221
|
+
|
196
222
|
def textnode?
|
197
223
|
false
|
198
224
|
end
|
@@ -208,7 +234,7 @@ module CSL
|
|
208
234
|
|
209
235
|
# Tests whether or not the Name matches the passed-in node name and
|
210
236
|
# attribute conditions; if a Hash is passed as a single argument,
|
211
|
-
# it is taken as the conditions parameter (the name parameter
|
237
|
+
# it is taken as the conditions parameter (the name parameter
|
212
238
|
# automatically matches in this case).
|
213
239
|
#
|
214
240
|
# Whether or not the arguments match the node is determined as
|
@@ -225,6 +251,7 @@ module CSL
|
|
225
251
|
#
|
226
252
|
# @see #exact_match?
|
227
253
|
#
|
254
|
+
# If the optional
|
228
255
|
# @param name [String,Regexp] must match the nodename
|
229
256
|
# @param conditions [Hash] the conditions
|
230
257
|
#
|
@@ -276,6 +303,21 @@ module CSL
|
|
276
303
|
end
|
277
304
|
alias matches_exactly? exact_match?
|
278
305
|
|
306
|
+
# @option filter [Array] a list of attribute names
|
307
|
+
# @return [Hash] the node's attributes matching the filter
|
308
|
+
def attributes_for(*filter)
|
309
|
+
filter.flatten!
|
310
|
+
|
311
|
+
Hash[map { |name, value|
|
312
|
+
!value.nil? && filter.include?(name) ? [name, value.to_s] : nil
|
313
|
+
}.compact]
|
314
|
+
end
|
315
|
+
|
316
|
+
# @return [Hash] the node's formatting options
|
317
|
+
def formatting_options
|
318
|
+
attributes_for Schema.attr(:formatting)
|
319
|
+
end
|
320
|
+
|
279
321
|
def <=>(other)
|
280
322
|
[nodename, attributes, children] <=> [other.nodename, other.attributes, other.children]
|
281
323
|
rescue
|
@@ -311,7 +353,7 @@ module CSL
|
|
311
353
|
|
312
354
|
def attribute_assignments
|
313
355
|
each_pair.map { |name, value|
|
314
|
-
value.nil? ? nil: [name, value.to_s.inspect].join('=')
|
356
|
+
value.nil? ? nil : [name, value.to_s.inspect].join('=')
|
315
357
|
}.compact
|
316
358
|
end
|
317
359
|
|
data/lib/csl/schema.rb
CHANGED
@@ -86,6 +86,10 @@ module CSL
|
|
86
86
|
})
|
87
87
|
|
88
88
|
@attributes.each_value { |v| v.map!(&:to_sym).freeze }
|
89
|
+
|
90
|
+
@attributes[:formatting] = [:'text-case'].concat(
|
91
|
+
@attributes.values_at(:affixes, :quotes, :font).flatten)
|
92
|
+
|
89
93
|
@attributes.freeze
|
90
94
|
|
91
95
|
@file = File.expand_path('../../../vendor/schema/csl.rng', __FILE__)
|
data/lib/csl/style/number.rb
CHANGED
@@ -4,7 +4,8 @@ module CSL
|
|
4
4
|
class Number < Node
|
5
5
|
attr_struct :variable, :form, :'text-case',
|
6
6
|
*Schema.attr(:affixes, :display, :font)
|
7
|
-
|
7
|
+
|
8
|
+
has_no_children
|
8
9
|
|
9
10
|
# @return [Boolean] whether or not the number's format is set to
|
10
11
|
# :numeric; also returns true if the number's form attribute is not
|
data/lib/csl/style/text.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
module CSL
|
2
2
|
class Style
|
3
|
-
|
3
|
+
|
4
4
|
class Text < Node
|
5
|
-
attr_struct :variable, :macro, :term, :form, :plural, :
|
6
|
-
|
5
|
+
attr_struct :variable, :macro, :term, :form, :plural, :value,
|
6
|
+
*Schema.attr(:formatting, :display, :periods)
|
7
|
+
|
8
|
+
has_no_children
|
7
9
|
end
|
8
|
-
|
10
|
+
|
9
11
|
end
|
10
12
|
end
|