<commit_before>module MoneyRails
  module ActiveModel
    class MoneyValidator < ::ActiveModel::Validations::NumericalityValidator
      def validate_each(record, attr, value)
        reset_memoized_variables!
        @record = record
        @attr = attr

        subunit_attr = @record.class.monetized_attributes[@attr.to_s]

        # WARNING: Currently this is only defined in ActiveRecord extension!
        before_type_cast = :"#{@attr}_money_before_type_cast"
        @raw_value = @record.try(before_type_cast)

        # If raw value is nil and changed subunit is nil, then
        # nil is a assigned value, else we should treat the
        # subunit value as the one assigned.
        if @raw_value.nil? && @record.public_send(subunit_attr)
          subunit_value = @record.public_send(subunit_attr)
          @raw_value = subunit_value.to_f / currency.subunit_to_unit
        end

        return if options[:allow_nil] && @raw_value.nil?

        # Skip normalization for Numeric values
        # which can directly be handled by NumericalityValidator
        if raw_value_is_non_numeric
          # remove currency symbol, and negative sign
          @raw_value = @raw_value.to_s.strip.gsub(symbol, "")

          add_error and return if value_has_too_many_decimal_points
          add_error if thousand_separator_after_decimal_mark
          add_error if invalid_thousands_separation

        end

        normalize_raw_value!
        super(@record, @attr, @raw_value)
      end

      private

      def reset_memoized_variables!
        [:currency, :decimal_mark, :thousands_separator, :symbol,
          :abs_raw_value, :decimal_pieces, :pieces_array].each do |var_name|
          ivar_name = :"@_#{var_name}"
          remove_instance_variable(ivar_name) if instance_variable_get(ivar_name)
        end
      end

      def currency
        @_currency ||= @record.public_send("currency_for_#{@attr}")
      end

      def decimal_mark
        character = currency.decimal_mark || '.'
        @_decimal_mark ||= Money.use_i18n ? I18n.t('number.currency.format.separator', default: character) : character
      end

      def thousands_separator
        character = currency.thousands_separator || ','
        @_thousands_separator ||= Money.use_i18n ? I18n.t('number.currency.format.delimiter', default: character) : character
      end

      def symbol
        @_symbol ||= I18n.t('number.currency.format.unit', default: currency.symbol)
      end

      def raw_value_is_non_numeric
        @raw_value.present? && !@raw_value.is_a?(Numeric)
      end

      def abs_raw_value
        @_abs_raw_value ||= @raw_value.strip.sub(/^-/, "")
      end

      def add_error
        attr_name = @attr.to_s.tr('.', '_').humanize
        attr_name = @record.class.human_attribute_name(@attr, default: attr_name)

        @record.errors.add(@attr, I18n.t('errors.messages.invalid_currency',
                                       { :thousands => thousands_separator,
                                         :decimal => decimal_mark,
                                         :currency => abs_raw_value,
                                         :attribute => attr_name }))
      end

      def decimal_pieces
        @_decimal_pieces ||= abs_raw_value.split(decimal_mark)
      end

      def value_has_too_many_decimal_points
        ![1, 2].include?(decimal_pieces.length)
      end

      def pieces_array
        @_pieces_array ||= decimal_pieces[0].split(thousands_separator.presence)
      end

      def thousand_separator_after_decimal_mark
        thousands_separator.present? && decimal_pieces.length == 2 && decimal_pieces[1].include?(thousands_separator)
      end

      def invalid_thousands_separation
        return false if pieces_array.length <= 1
        return true  if pieces_array[0].length > 3
        pieces_array[1..-1].any? do |thousands_group|
          thousands_group.length != 3
        end
      end

      # Remove thousands separators, normalize decimal mark,
      # remove whitespaces and _ (E.g. 99 999 999 or 12_300_200.20)
      def normalize_raw_value!
        @raw_value = @raw_value.to_s
          .gsub(thousands_separator, '')
          .gsub(decimal_mark, '.')
          .gsub(/[\s_]/, '')
      end
    end
  end
end

# Compatibility with ActiveModel validates method which matches option keys to their validator class<commit_msg>Make validation a little less brittle<commit_after>module MoneyRails
  module ActiveModel
    class MoneyValidator < ::ActiveModel::Validations::NumericalityValidator
      def validate_each(record, attr, value)
        reset_memoized_variables!
        @record = record
        @attr = attr

        subunit_attr = @record.class.monetized_attributes[@attr.to_s]

        # WARNING: Currently this is only defined in ActiveRecord extension!
        before_type_cast = :"#{@attr}_money_before_type_cast"
        @raw_value = @record.try(before_type_cast)

        # If raw value is nil and changed subunit is nil, then
        # nil is a assigned value, else we should treat the
        # subunit value as the one assigned.
        if @raw_value.nil? && @record.public_send(subunit_attr)
          subunit_value = @record.public_send(subunit_attr)
          @raw_value = subunit_value.to_f / currency.subunit_to_unit
        end

        return if options[:allow_nil] && @raw_value.nil?

        # Set this before we modify @raw_value below.
        stringy = @raw_value.present? && !@raw_value.is_a?(Numeric)

        # Skip normalization for Numeric values
        # which can directly be handled by NumericalityValidator
        if stringy
          # remove currency symbol
          @raw_value = @raw_value.to_s.gsub(symbol, "")
          add_error and return if value_has_too_many_decimal_points
          add_error if thousand_separator_after_decimal_mark
          add_error if invalid_thousands_separation

        end

        normalize_raw_value!
        super(@record, @attr, @raw_value)
      end

      private

      def reset_memoized_variables!
        [:currency, :decimal_mark, :thousands_separator, :symbol,
          :abs_raw_value, :decimal_pieces, :pieces_array].each do |var_name|
          ivar_name = :"@_#{var_name}"
          remove_instance_variable(ivar_name) if instance_variable_defined?(ivar_name)
        end
      end

      def currency
        @_currency ||= @record.public_send("currency_for_#{@attr}")
      end

      def decimal_mark
        character = currency.decimal_mark || '.'
        @_decimal_mark ||= Money.use_i18n ? I18n.t('number.currency.format.separator', default: character) : character
      end

      def thousands_separator
        character = currency.thousands_separator || ','
        @_thousands_separator ||= Money.use_i18n ? I18n.t('number.currency.format.delimiter', default: character) : character
      end

      def symbol
        @_symbol ||= I18n.t('number.currency.format.unit', default: currency.symbol)
      end

      def abs_raw_value
        @_abs_raw_value ||= @raw_value.to_s.sub(/^\s*-/, "").strip
      end

      def add_error
        attr_name = @attr.to_s.tr('.', '_').humanize
        attr_name = @record.class.human_attribute_name(@attr, default: attr_name)

        @record.errors.add(@attr, I18n.t('errors.messages.invalid_currency',
                                       { :thousands => thousands_separator,
                                         :decimal => decimal_mark,
                                         :currency => abs_raw_value,
                                         :attribute => attr_name }))
      end

      def decimal_pieces
        @_decimal_pieces ||= abs_raw_value.split(decimal_mark)
      end

      def value_has_too_many_decimal_points
        ![1, 2].include?(decimal_pieces.length)
      end

      def pieces_array
        @_pieces_array ||= decimal_pieces[0].split(thousands_separator.presence)
      end

      def thousand_separator_after_decimal_mark
        thousands_separator.present? && decimal_pieces.length == 2 && decimal_pieces[1].include?(thousands_separator)
      end

      def invalid_thousands_separation
        return false if pieces_array.length <= 1
        return true  if pieces_array[0].length > 3
        pieces_array[1..-1].any? do |thousands_group|
          thousands_group.length != 3
        end
      end

      # Remove thousands separators, normalize decimal mark,
      # remove whitespaces and _ (E.g. 99 999 999 or 12_300_200.20)
      def normalize_raw_value!
        # Cache abs_raw_value before normalizing because it's used in
        # many places and relies on the original @raw_value.
        abs_raw_value

        @raw_value = @raw_value.to_s
          .gsub(thousands_separator, '')
          .gsub(decimal_mark, '.')
          .gsub(/[\s_]/, '')
      end
    end
  end
end

# Compatibility with ActiveModel validates method which matches option keys to their validator class
{'fraction_non_alphanumeric': 0.0881200313514724, 'fraction_numerical': 0.00918150263128429, 'mean_word_length': 2.422222222222222, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2481231', 'n_tokens_mistral': 2731, 'n_tokens_neox': 2563, 'n_words': 570}