csl 1.0.0.pre5 → 1.0.0.pre6
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.
- 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
|