Class: Csvlint::Csvw::NumberFormat

Inherits:
Object
  • Object
show all
Defined in:
lib/csvlint/csvw/number_format.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pattern = nil, grouping_separator = nil, decimal_separator = ".", integer = nil) ⇒ NumberFormat

Returns a new instance of NumberFormat.



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
# File 'lib/csvlint/csvw/number_format.rb', line 6

def initialize(pattern = nil, grouping_separator = nil, decimal_separator = ".", integer = nil)
  @pattern = pattern
  @integer = integer
  if @integer.nil?
    @integer = if @pattern.nil?
      nil
    else
      !@pattern.include?(decimal_separator)
    end
  end
  @grouping_separator = grouping_separator || (@pattern.nil? ? nil : ",")
  @decimal_separator = decimal_separator || "."
  if pattern.nil?
    @regexp = if integer
      INTEGER_REGEXP
    else
      Regexp.new("^(([-+]?[0-9]+(\\.[0-9]+)?([Ee][-+]?[0-9]+)?[%‰]?)|NaN|INF|-INF)$")
    end
  else
    numeric_part_regexp = Regexp.new("(?<numeric_part>[-+]?([0#Ee]|#{Regexp.escape(@grouping_separator)}|#{Regexp.escape(@decimal_separator)})+)")
    number_format_regexp = Regexp.new("^(?<prefix>.*?)#{numeric_part_regexp}(?<suffix>.*?)$")
    match = number_format_regexp.match(pattern)
    raise Csvw::NumberFormatError, "invalid number format" if match.nil?

    @prefix = match["prefix"]
    @numeric_part = match["numeric_part"]
    @suffix = match["suffix"]

    parts = @numeric_part.split("E")
    mantissa_part = parts[0]
    exponent_part = parts[1] || ""
    mantissa_parts = mantissa_part.split(@decimal_separator)
    # raise Csvw::NumberFormatError, "more than two decimal separators in number format" if parts.length > 2
    integer_part = mantissa_parts[0]
    fractional_part = mantissa_parts[1] || ""

    if ["+", "-"].include?(integer_part[0])
      numeric_part_regexp = "\\#{integer_part[0]}"
      integer_part = integer_part[1..-1]
    else
      numeric_part_regexp = "[-+]?"
    end

    min_integer_digits = integer_part.gsub(@grouping_separator, "").delete("#").length
    min_fraction_digits = fractional_part.gsub(@grouping_separator, "").delete("#").length
    max_fraction_digits = fractional_part.gsub(@grouping_separator, "").length
    min_exponent_digits = exponent_part.delete("#").length
    max_exponent_digits = exponent_part.length

    integer_parts = integer_part.split(@grouping_separator)[1..-1]
    @primary_grouping_size = begin
      integer_parts[-1].length
    rescue
      0
    end
    @secondary_grouping_size = begin
      integer_parts[-2].length
    rescue
      @primary_grouping_size
    end

    fractional_parts = fractional_part.split(@grouping_separator)[0..-2]
    @fractional_grouping_size = begin
      fractional_parts[0].length
    rescue
      0
    end

    if @primary_grouping_size == 0
      integer_regexp = "[0-9]*[0-9]{#{min_integer_digits}}"
    else
      leading_regexp = "([0-9]{0,#{@secondary_grouping_size - 1}}#{Regexp.escape(@grouping_separator)})?"
      secondary_groups = "([0-9]{#{@secondary_grouping_size}}#{Regexp.escape(@grouping_separator)})*"
      if min_integer_digits > @primary_grouping_size
        remaining_req_digits = min_integer_digits - @primary_grouping_size
        req_secondary_groups = (remaining_req_digits / @secondary_grouping_size > 0) ? "([0-9]{#{@secondary_grouping_size}}#{Regexp.escape(@grouping_separator)}){#{remaining_req_digits / @secondary_grouping_size}}" : ""
        if remaining_req_digits % @secondary_grouping_size > 0
          final_req_digits = "[0-9]{#{@secondary_grouping_size - (remaining_req_digits % @secondary_grouping_size)}}"
          final_opt_digits = "[0-9]{0,#{@secondary_grouping_size - (remaining_req_digits % @secondary_grouping_size)}}"
          integer_regexp = "((#{leading_regexp}#{secondary_groups}#{final_req_digits})|#{final_opt_digits})[0-9]{#{remaining_req_digits % @secondary_grouping_size}}#{Regexp.escape(@grouping_separator)}#{req_secondary_groups}[0-9]{#{@primary_grouping_size}}"
        else
          integer_regexp = "(#{leading_regexp}#{secondary_groups})?#{req_secondary_groups}[0-9]{#{@primary_grouping_size}}"
        end
      else
        final_req_digits = (@primary_grouping_size > min_integer_digits) ? "[0-9]{#{@primary_grouping_size - min_integer_digits}}" : ""
        final_opt_digits = (@primary_grouping_size > min_integer_digits) ? "[0-9]{0,#{@primary_grouping_size - min_integer_digits}}" : ""
        integer_regexp = "((#{leading_regexp}#{secondary_groups}#{final_req_digits})|#{final_opt_digits})[0-9]{#{min_integer_digits}}"
      end
    end

    numeric_part_regexp += integer_regexp

    if max_fraction_digits > 0
      if @fractional_grouping_size == 0
        fractional_regexp = ""
        fractional_regexp += "[0-9]{#{min_fraction_digits}}" if min_fraction_digits > 0
        fractional_regexp += "[0-9]{0,#{max_fraction_digits - min_fraction_digits}}" unless min_fraction_digits == max_fraction_digits
        fractional_regexp = "#{Regexp.escape(@decimal_separator)}#{fractional_regexp}"
        fractional_regexp = "(#{fractional_regexp})?" if min_fraction_digits == 0
        numeric_part_regexp += fractional_regexp
      else
        fractional_regexp = ""

        if min_fraction_digits > 0
          if min_fraction_digits >= @fractional_grouping_size
            # first group of required digits - something like "[0-9]{3}"
            fractional_regexp += "[0-9]{#{@fractional_grouping_size}}"
            # additional groups of required digits - something like "(,[0-9]{3}){1}"
            fractional_regexp += "(#{Regexp.escape(@grouping_separator)}[0-9]{#{@fractional_grouping_size}}){#{min_fraction_digits / @fractional_grouping_size - 1}}" if min_fraction_digits / @fractional_grouping_size > 1
            fractional_regexp += Regexp.escape(@grouping_separator).to_s if min_fraction_digits % @fractional_grouping_size > 0
          end
          # additional required digits - something like ",[0-9]{1}"
          fractional_regexp += "[0-9]{#{min_fraction_digits % @fractional_grouping_size}}" if min_fraction_digits % @fractional_grouping_size > 0

          opt_fractional_digits = max_fraction_digits - min_fraction_digits
          if opt_fractional_digits > 0
            fractional_regexp += "("

            if min_fraction_digits % @fractional_grouping_size > 0
              # optional fractional digits to complete the group
              fractional_regexp += "[0-9]{0,#{[opt_fractional_digits, @fractional_grouping_size - (min_fraction_digits % @fractional_grouping_size)].min}}"
              fractional_regexp += "|"
              fractional_regexp += "[0-9]{#{[opt_fractional_digits, @fractional_grouping_size - (min_fraction_digits % @fractional_grouping_size)].min}}"
            else
              fractional_regexp += "(#{Regexp.escape(@grouping_separator)}[0-9]{1,#{@fractional_grouping_size}})?"
              fractional_regexp += "|"
              fractional_regexp += "#{Regexp.escape(@grouping_separator)}[0-9]{#{@fractional_grouping_size}}"
            end

            remaining_opt_fractional_digits = opt_fractional_digits - (@fractional_grouping_size - (min_fraction_digits % @fractional_grouping_size))
            if remaining_opt_fractional_digits > 0
              if remaining_opt_fractional_digits % @fractional_grouping_size > 0
                # optional fraction digits in groups
                fractional_regexp += "(#{Regexp.escape(@grouping_separator)}[0-9]{#{@fractional_grouping_size}}){0,#{remaining_opt_fractional_digits / @fractional_grouping_size}}" if remaining_opt_fractional_digits > @fractional_grouping_size
                # remaining optional fraction digits
                fractional_regexp += "(#{Regexp.escape(@grouping_separator)}[0-9]{1,#{remaining_opt_fractional_digits % @fractional_grouping_size}})?"
              else
                # optional fraction digits in groups
                fractional_regexp += "(#{Regexp.escape(@grouping_separator)}[0-9]{#{@fractional_grouping_size}}){0,#{(remaining_opt_fractional_digits / @fractional_grouping_size) - 1}}" if remaining_opt_fractional_digits > @fractional_grouping_size
                # remaining optional fraction digits
                fractional_regexp += "(#{Regexp.escape(@grouping_separator)}[0-9]{1,#{@fractional_grouping_size}})?"
              end

              # optional fraction digits in groups
              fractional_regexp += "(#{Regexp.escape(@grouping_separator)}[0-9]{#{@fractional_grouping_size}}){0,#{(remaining_opt_fractional_digits / @fractional_grouping_size) - 1}}" if remaining_opt_fractional_digits > @fractional_grouping_size
              # remaining optional fraction digits
              fractional_regexp += "(#{Regexp.escape(@grouping_separator)}[0-9]{1,#{remaining_opt_fractional_digits % @fractional_grouping_size}})?" if remaining_opt_fractional_digits % @fractional_grouping_size > 0
            end
            fractional_regexp += ")"
          end
        elsif max_fraction_digits % @fractional_grouping_size > 0
          # optional fractional digits in groups
          fractional_regexp += "([0-9]{#{@fractional_grouping_size}}#{Regexp.escape(@grouping_separator)}){0,#{max_fraction_digits / @fractional_grouping_size}}"
          # remaining optional fraction digits
          fractional_regexp += "(#{Regexp.escape(@grouping_separator)}[0-9]{1,#{max_fraction_digits % @fractional_grouping_size}})?" if max_fraction_digits % @fractional_grouping_size > 0
        else
          fractional_regexp += "([0-9]{#{@fractional_grouping_size}}#{Regexp.escape(@grouping_separator)}){0,#{(max_fraction_digits / @fractional_grouping_size) - 1}}" if max_fraction_digits > @fractional_grouping_size
          fractional_regexp += "[0-9]{1,#{@fractional_grouping_size}}"
        end
        fractional_regexp = "#{Regexp.escape(@decimal_separator)}#{fractional_regexp}"
        fractional_regexp = "(#{fractional_regexp})?" if min_fraction_digits == 0
        numeric_part_regexp += fractional_regexp
      end
    end

    if max_exponent_digits > 0
      numeric_part_regexp += "E"
      numeric_part_regexp += "[0-9]{0,#{max_exponent_digits - min_exponent_digits}}" unless max_exponent_digits == min_exponent_digits
      numeric_part_regexp += "[0-9]{#{min_exponent_digits}}" unless min_exponent_digits == 0
    end

    @regexp = Regexp.new("^(?<prefix>#{Regexp.escape(@prefix)})(?<numeric_part>#{numeric_part_regexp})(?<suffix>#{suffix})$")
  end
end

Instance Attribute Details

#decimal_separatorObject (readonly)

Returns the value of attribute decimal_separator.



4
5
6
# File 'lib/csvlint/csvw/number_format.rb', line 4

def decimal_separator
  @decimal_separator
end

#fractional_grouping_sizeObject (readonly)

Returns the value of attribute fractional_grouping_size.



4
5
6
# File 'lib/csvlint/csvw/number_format.rb', line 4

def fractional_grouping_size
  @fractional_grouping_size
end

#grouping_separatorObject (readonly)

Returns the value of attribute grouping_separator.



4
5
6
# File 'lib/csvlint/csvw/number_format.rb', line 4

def grouping_separator
  @grouping_separator
end

#integerObject (readonly)

Returns the value of attribute integer.



4
5
6
# File 'lib/csvlint/csvw/number_format.rb', line 4

def integer
  @integer
end

#numeric_partObject (readonly)

Returns the value of attribute numeric_part.



4
5
6
# File 'lib/csvlint/csvw/number_format.rb', line 4

def numeric_part
  @numeric_part
end

#patternObject (readonly)

Returns the value of attribute pattern.



4
5
6
# File 'lib/csvlint/csvw/number_format.rb', line 4

def pattern
  @pattern
end

#prefixObject (readonly)

Returns the value of attribute prefix.



4
5
6
# File 'lib/csvlint/csvw/number_format.rb', line 4

def prefix
  @prefix
end

#primary_grouping_sizeObject (readonly)

Returns the value of attribute primary_grouping_size.



4
5
6
# File 'lib/csvlint/csvw/number_format.rb', line 4

def primary_grouping_size
  @primary_grouping_size
end

#secondary_grouping_sizeObject (readonly)

Returns the value of attribute secondary_grouping_size.



4
5
6
# File 'lib/csvlint/csvw/number_format.rb', line 4

def secondary_grouping_size
  @secondary_grouping_size
end

#suffixObject (readonly)

Returns the value of attribute suffix.



4
5
6
# File 'lib/csvlint/csvw/number_format.rb', line 4

def suffix
  @suffix
end

Instance Method Details

#match(value) ⇒ Object



181
182
183
# File 'lib/csvlint/csvw/number_format.rb', line 181

def match(value)
  value&.match?(@regexp) ? true : false
end

#parse(value) ⇒ Object



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
# File 'lib/csvlint/csvw/number_format.rb', line 185

def parse(value)
  if @pattern.nil?
    return nil if !@grouping_separator.nil? && value =~ Regexp.new("((^#{Regexp.escape(@grouping_separator)})|#{Regexp.escape(@grouping_separator)}{2})")
    value.gsub!(@grouping_separator, "") unless @grouping_separator.nil?
    value.gsub!(@decimal_separator, ".") unless @decimal_separator.nil?
    if value&.match?(@regexp)
      case value
      when "NaN"
        Float::NAN
      when "INF"
        Float::INFINITY
      when "-INF"
        -Float::INFINITY
      else
        case value[-1]
        when "%"
          value.to_f / 100
        when ""
          value.to_f / 1000
        else
          if @integer.nil?
            value.include?(".") ? value.to_f : value.to_i
          else
            @integer ? value.to_i : value.to_f
          end
        end
      end
    end
  else
    match = @regexp.match(value)
    return nil if match.nil?
    number = match["numeric_part"]
    number.gsub!(@grouping_separator, "") unless @grouping_separator.nil?
    number.gsub!(@decimal_separator, ".") unless @decimal_separator.nil?
    number = @integer ? number.to_i : number.to_f
    number = number.to_f / 100 if match["prefix"].include?("%") || match["suffix"].include?("%")
    number = number.to_f / 1000 if match["prefix"].include?("") || match["suffix"].include?("")
    number
  end
end