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.
@@ -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
@@ -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.exact_match?(query) }
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 is
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__)
@@ -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
@@ -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, :'text-case',
6
- :value, *Schema.attr(:affixes, :display, :font, :quotes, :periods)
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