Module: SmarterCSV::HashTransformations

Included in:
Reader
Defined in:
lib/smarter_csv/hash_transformations.rb

Constant Summary collapse

NUMERIC_REGEX =

Frozen regex constants for performance (avoid recompilation on every value)

/\A[+-]?\d+(?:\.\d+)?\z/.freeze
ZERO_REGEX =

FLOAT_REGEX = /A?d+.d+z/.freeze INTEGER_REGEX = /A?d+z/.freeze

/\A[+-]?0+(?:\.0+)?\z/.freeze
ZERO_BYTE =

First-byte values that can begin a numeric literal — used to skip the numeric regexes for values that obviously aren’t numbers (e.g. city names).

'0'.ord
NINE_BYTE =

48

'9'.ord
PLUS_BYTE =

57

'+'.ord
MINUS_BYTE =

43

'-'.ord

Instance Method Summary collapse

Instance Method Details

#hash_transformations(hash, options) ⇒ Object

45



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
# File 'lib/smarter_csv/hash_transformations.rb', line 18

def hash_transformations(hash, options)
  # Modify hash in-place for performance (avoids allocating a second hash per row)

  # Remove nil/empty keys
  hash.delete(nil)
  hash.delete('')
  hash.delete(:"")

  remove_empty_values = options[:remove_empty_values] == true
  remove_zero_values = options[:remove_zero_values]
  nil_values_matching = options[:nil_values_matching]
  convert_to_numeric = options[:convert_values_to_numeric]
  value_converters = options[:value_converters]

  # Early return if no transformations needed
  return hash unless remove_empty_values || remove_zero_values || nil_values_matching || convert_to_numeric || value_converters

  # {only:}/{except:} limits on numeric conversion apply only when the option is a Hash;
  # in the common case (true/false) skip the per-key check entirely.
  numeric_has_limits = convert_to_numeric.is_a?(Hash)
  rails = has_rails
  keys_to_delete = nil # lazily allocated only if something is actually removed

  hash.each do |k, v|
    # Nil-ify values matching the pattern (keeps the key; remove_empty_values handles deletion)
    if nil_values_matching
      str_val = v.is_a?(String) ? v : (v.is_a?(Numeric) ? v.to_s : nil)
      if str_val && nil_values_matching.match?(str_val)
        hash[k] = nil
        v = nil
        # fall through: remove_empty_values will delete the key if true
      end
    end

    # Check if this key/value should be removed
    # Note: numeric values (Integer/Float) are never blank, so skip the blank check for them
    if remove_empty_values && !v.is_a?(Numeric) && (rails ? v.blank? : blank?(v))
      (keys_to_delete ||= []) << k
      next
    end

    # Handle both string zeros ("0", "0.0") and numeric zeros (already converted by C)
    if remove_zero_values && ((v.is_a?(String) && ZERO_REGEX.match?(v)) || (v.is_a?(Numeric) && v == 0))
      (keys_to_delete ||= []) << k
      next
    end

    # Convert to numeric if requested
    if convert_to_numeric && v.is_a?(String) &&
       (!numeric_has_limits || !limit_execution_for_only_or_except(options, :convert_values_to_numeric, k))
      # Fast-reject: the string is already stripped and NUMERIC_REGEX is \A-anchored on a digit or sign,
      # so a value whose first byte isn't a digit, '+', or '-' cannot be numeric — skip the regex entirely.
      first_byte = v.getbyte(0)
      if first_byte && ((first_byte >= ZERO_BYTE && first_byte <= NINE_BYTE) || first_byte == MINUS_BYTE || first_byte == PLUS_BYTE)
        if NUMERIC_REGEX.match?(v)
          hash[k] = v.include?('.') ? v.to_f : v.to_i
        end
      end
    end

    # Apply value converters
    if value_converters
      converter = value_converters[k]
      hash[k] = converter.respond_to?(:convert) ? converter.convert(hash[k]) : converter.call(hash[k]) if converter
    end
  end

  # Delete marked keys
  keys_to_delete&.each { |key| hash.delete(key) }

  hash
end