<commit_before>class Curryable
  def initialize(command_class, *arguments)
    @command_class = command_class
    @arguments = arguments
  end

  attr_reader :command_class, :arguments
  private     :command_class, :arguments

  def call(*new_arguments)
    curryable = self.class.new(
      command_class,
      *combined_arguments(new_arguments)
    )

    if curryable.enough_arguments?
      curryable.execute
    else
      curryable
    end
  end

  def inspect
    [
      "#<Curryable",
      "<",
      command_class.name,
      ">",
      ":0x",
      object_id.<<(1).to_s(16),
      " ",
      parameters_for_inspection,
      ">",
    ].join
  end

  protected

  def enough_arguments?
    enough_positional_arguments? && enough_keyword_arguments?
  end

  def execute
    command_class.new(*arguments).call
  end

  private

  def parameters_for_inspection
    [
      positional_parameters_for_inspection,
      keyword_parameters_for_inspection,
    ].reject(&:empty?).join(", ")
  end

  class SweetNothing
    def inspect
      ""
    end
  end

  def positional_parameters_for_inspection
    nothings = [SweetNothing.new] * positional_parameters.length
    positional_parameter_names.zip(arguments + nothings).map { |name, value|
      "#{name.to_s}=#{value.inspect}"
    }.join(", ")
  end

  def keyword_parameters_for_inspection
    required_keywords
      .map { |name|
        [
          name,
          provided_keyword_arguments.fetch(name, SweetNothing.new)
        ]
      }
      .map { |name, value|
        "#{name.to_s}:#{value.inspect}"
      }
      .join(", ")
  end

  def positional_parameter_names
    positional_parameters.map(&:last)
  end

  def positional_parameters
    parameters.select { |(type, _name)| type == :req }
  end

  def combined_arguments(new_arguments)
    combined = arguments + new_arguments

    positional = combined.take(arity)

    possible_keywords = combined.drop(arity)

    unless possible_keywords.all? { |arg| arg.is_a?(Hash) }
      excess_arg_count = combined.length

      raise ArgumentError.new(
        "wrong number of arguments (#{excess_arg_count} for #{arity})"
      )
    end

    keywords = possible_keywords.reduce(&:merge) || {}

    unknown_keywords = keywords.keys - required_keywords

    if unknown_keywords.any?
      plural = unknown_keywords.length > 1 ? "s" : ""

      raise ArgumentError.new(
        "unknown keyword#{plural}: #{unknown_keywords.join(", ")}"
      )
    end

    positional + [keywords].reject(&:empty?)
  end

  def enough_positional_arguments?
    arguments.length >= arity
  end

  def enough_keyword_arguments?
    provided_keywords & required_keywords == required_keywords
  end

  def provided_keywords
    provided_keyword_arguments.keys
  end

  def provided_keyword_arguments
    arguments.drop(arity).fetch(0, {})
  end

  def required_keywords
    parameters
      .select { |(type, _name)| type == :keyreq }
      .map { |(_type, name)| name }
  end

  def arity
    parameters.count { |(type, _name)| type == :req }
  end

  def parameters
    command_class.instance_method(:initialize).parameters
  end
end
<commit_msg>Introduce parameter list abstraction<commit_after>class Curryable
  def initialize(command_class, *arguments)
    @command_class = command_class
    @arguments = arguments
  end

  attr_reader :command_class, :arguments
  private     :command_class, :arguments

  def call(*new_arguments)
    curryable = self.class.new(
      command_class,
      *combined_arguments(new_arguments)
    )

    if curryable.enough_arguments?
      curryable.execute
    else
      curryable
    end
  end

  def inspect
    [
      "#<Curryable",
      "<",
      command_class.name,
      ">",
      ":0x",
      object_id.<<(1).to_s(16),
      " ",
      parameters_for_inspection,
      ">",
    ].join
  end

  protected

  def enough_arguments?
    enough_positional_arguments? && enough_keyword_arguments?
  end

  def execute
    command_class.new(*arguments).call
  end

  private

  def parameters_for_inspection
    [
      positional_parameters_for_inspection,
      keyword_parameters_for_inspection,
    ].reject(&:empty?).join(", ")
  end

  class SweetNothing
    def inspect
      ""
    end
  end

  def positional_parameters_for_inspection
    nothings = [SweetNothing.new] * positional_parameters.length
    positional_parameter_names.zip(arguments + nothings).map { |name, value|
      "#{name.to_s}=#{value.inspect}"
    }.join(", ")
  end

  def keyword_parameters_for_inspection
    required_keywords
      .map { |name|
        [
          name,
          provided_keyword_arguments.fetch(name, SweetNothing.new)
        ]
      }
      .map { |name, value|
        "#{name.to_s}:#{value.inspect}"
      }
      .join(", ")
  end

  def positional_parameter_names
    positional_parameters.map(&:name)
  end

  def positional_parameters
    parameters.positional
  end

  def combined_arguments(new_arguments)
    combined = arguments + new_arguments

    positional = combined.take(arity)

    possible_keywords = combined.drop(arity)

    unless possible_keywords.all? { |arg| arg.is_a?(Hash) }
      excess_arg_count = combined.length

      raise ArgumentError.new(
        "wrong number of arguments (#{excess_arg_count} for #{arity})"
      )
    end

    keywords = possible_keywords.reduce(&:merge) || {}

    unknown_keywords = keywords.keys - required_keywords

    if unknown_keywords.any?
      plural = unknown_keywords.length > 1 ? "s" : ""

      raise ArgumentError.new(
        "unknown keyword#{plural}: #{unknown_keywords.join(", ")}"
      )
    end

    positional + [keywords].reject(&:empty?)
  end

  def enough_positional_arguments?
    arguments.length >= arity
  end

  def enough_keyword_arguments?
    provided_keywords & required_keywords == required_keywords
  end

  def provided_keywords
    provided_keyword_arguments.keys
  end

  def provided_keyword_arguments
    arguments.drop(arity).fetch(0, {})
  end

  def required_keywords
    parameters.required_keywords.map(&:name)
  end

  def arity
    parameters.arity
  end

  def parameters
    ParameterList.new(command_class.instance_method(:initialize).parameters)
  end

  class ParameterList
    def initialize(list)
      @raw_list = list
    end

    def arity
      required_positional.count
    end

    def positional
      list.select(&:positional?)
    end

    def required_positional
      positional.select(&:required?)
    end

    def required_keywords
      list.select(&:keyword?).select(&:required?)
    end

    private

    def list
      @list ||= @raw_list.map { |type, name| Parameter.new(type, name) }
    end

    class Parameter
      def initialize(type, name)
        @type = type
        @name = name
      end

      attr_reader :type, :name

      def required?
        type == :req || type == :keyreq
      end

      def positional?
        type == :req
      end

      def keyword?
        type == :keyreq
      end
    end
  end
end

{'fraction_non_alphanumeric': 0.09858753031816236, 'fraction_numerical': 0.0017120844628334998, 'mean_word_length': 2.728723404255319, 'pattern_counts': {'":': 2, '<': 11, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3464289', 'n_tokens_mistral': 2224, 'n_tokens_neox': 2118, 'n_words': 429}