Jordan
Jordan

Reputation: 32522

Shorter way to convert an empty string to an int, and then clamp

I'm looking for a way to simplify the code for the following logic:

  1. Take a value that is either a nil or an empty string
  2. Convert that value to an integer
  3. Set zero values to the maximum value (empty string/nil are converted to 0 when cast as an int)
  4. .clamp the value between a minimum and a maximum

Here's the long form that works:

minimum = 1
maximum = 10_000
value = value.to_i
value = maximum if value.zero?
value = value.clamp(minimum, maximum)

So for example, if value is "", I should get 10,000. If value is "15", I should get 15. If value is "45000", I should get 10000.

Is there a way to shorten this logic, assuming that minimum and maximum are defined and that the default value is the maximum?

The biggest problem I've had in shortening it is that null-coalescing doesn't work on the zero, since Ruby considers zero a truthy value. Otherwise, it could be a one-liner.

Upvotes: 0

Views: 427

Answers (2)

Todd A. Jacobs
Todd A. Jacobs

Reputation: 84343

Consider Fixing the Input Object or Method

If you're messing with String objects when you expect an Integer, you're probably dealing with user input. If that's the case, the problem should really be solved through input validation and/or looping over an input prompt elsewhere in your program rather than trying to perform input transformations inline.

Duck-typing is great, but I suspect you have a broken contract between methods or objects. As a general rule, it's better to fix the source of the mismatch unless you're deliberately wrapping some piece of code that shouldn't be modified. There are a number of possible refactorings and patterns if that's the case.

One such solution is to use a collaborator object or method for information hiding. This enables you to perform your input transformations without complicating your inline logic, and allowing you to access the transformed value as a simple method call such as user_input.value.

Turning a Value into a Collaborator Object

If you are just trying to tighten up your current method you can aim for shorter code, but I'd personally recommend aiming for maintainability instead. Pragmatically, that means sending your value to the constructor of a specialized object, and then asking that object for a result. As a bonus, this allows you to use a default variable assignment to handle nil. Consider the following:

class MaximizeUnsetInputValue
  MIN = 1
  MAX = 10_000

  def initialize value=MAX
    @value = value
    set_empty_to_max
  end

  def set_empty_to_max
    @value = MAX if @value.to_i.zero?
  end

  def value
    @value.clamp MIN, MAX
  end
end

You can easily validate that this handles your various use cases while hiding the implementation details inside the collaborator object's methods. For example:

inputs_and_expected_outputs = [
  [0, 10000],
  [1, 1],
  [10, 10],
  [10001, 10000],
  [nil, 10000],
  ['', 10000]
]

inputs_and_expected_outputs.map do |input, expected|
  MaximizeUnsetInputValue.new(input).value == expected
end
#=> [true, true, true, true, true, true]

There are certainly other approaches, but this is the one I'd recommend based on your posted code. It isn't shorter, but I think it's readable, maintainable, adaptable, and reusable. Your mileage may vary.

Upvotes: 1

Mshka
Mshka

Reputation: 1828

you could still do a one-liner with your current logic

    minimum, maximum = 1, 10_000
    value = ( value.to_i.zero? ? maximum: value.to_i ).clamp(minimum, maximum)

but not sure if your issue is that if you enter '0' you want 1 and not 10_000 if so then try this

minimum, maximum = 1, 10_000
value = (value.to_i if Float(value) rescue maximum).clamp(minimum, maximum)

Upvotes: 1

Related Questions