Laziness and Stupidity; or, Model Validation in Rails

Posted by jeff Sunday, May 18, 2008 00:47:00 GMT

I thought my post on how to customize the ActiveRecord error header messages was a one-off about how we, as web developers, should treat our users as humans. But it looks like this is turning into an occasional series.

Today’s Victim

I’m an avid fan of Consumer Reports. I’ve subscribed for about as long as I can remember. Subscribers get to take part in their annual survey, which they mainly use for their car ratings. I kind of hated in the past, since their survey site was so lame and hard to use. I think it might have been an old-school perl-driven cgi script, if I remember correctly.

But this year, I was pleasantly surprised that it had been given a nice facelift. From the url I can see that they’re now using something more modern (ASP.NET).

But then, about four questions in, I got this:

Don’t use commas? Are you kidding me?

This is the kind of crap that permeates the web and is just sheer stupidity.

Would it really be so hard for the .NET app to remove commas from whatever value I enter? (No, it wouldn’t be, it’s actually pretty easy… but I digress). My guess is, the database column is an integer column, and if there’s a comma in the number, the insert statement will fail. So instead of writing code to take commas out, they tell the user to make sure they only enter integer numbers.

This is amazing. Shame on CR for letting their developers get away with this.

Since I want to help CR by taking the survey, I made sure not to use commas and entered “5000” and clicked Next.

It Gets Even Worse

But curiosity got the better of me, and so I clicked “Previous” to go back. Then I entered “5,000” with the comma, just to see what would happen. I began to think, maybe the bark is worse than the bite? Surely, the asp.net code really could deal with commas in numbers, right?

Nope:

Look at that awful hideous generic error message. Listen to the tone: you did something wrong, you idiot! Now, fix it!

We’ve already gone over how to customize these kinds of error messages in Rails. But even better is to avoid this situation in the first place.

If this kind of thing is hard to do in your favorite framework, then switch to Rails. Because it’s really easy to do in Rails.

Your Mission, Should You Choose To Accept It

I was planning on doing a little “Validation 101” here to demonstrate how your Rails applications can elegantly clean up user input before it gets to the database.

But I thought it might be more fun this time around to let you do the talking. So here’s my challenge to everyone reading this blog:

  1. Leave a comment to this post, giving the most elegant solution you can think of to remove commas from a text field in Rails.
  2. You can assume that there is an ActiveRecord class named Vehicle that has an attribute named miles.
  3. IMPORTANT: Be sure to put your code inside of a big <pre> tag.

Bring it on!

Comments

Leave a response

  1. Tom Fakes   May 18, 2008 @ 01:27 AM

    Why do I need to put my code in a PRE tag? Surely the comment editor knows I’m going to be typing in code!

    Sorry, couldn’t resist

  2. Arron   May 18, 2008 @ 01:39 AM
    Assuming vehicle is an instance of Vehicle and there’s an appropriate error checking before hand, then the only important line of code is this:
    vehicle.miles = param[:vehicle][:miles].gsub(/,/, '').to_i
  3. Arron   May 18, 2008 @ 01:43 AM

    But, this is pretty lame for a challenge.

    Change #1 to be, “the most elegant solution to parse HUMAN numbers” and it’ll be a little better.

    Human numbers being the stuff people normally enter, like “15,000” or “15,000KM” or “15,000 miles”

  4. Chris Lloyd   May 18, 2008 @ 02:00 AM

    class Vehicle < ActiveRecord::Base before_save :normalize_miles end

    def normalize_miles
      self.miles.gsub!(',','')
    end
  5. Luke Hartman   May 18, 2008 @ 02:00 AM

    In the Vehicle model:

    def miles=(inputted_miles)
        write_attribute(:miles, String(inputted_miles).delete("^0-9"))
     end
    

    This takes the user-given value and strips all non-digits (commas and units) from it. Another method could be used to format the value when retrieved (def miles … end)

  6. Phil Weber   May 18, 2008 @ 05:12 AM
    self.miles = miles.gsub(/[^0-9]/, "") 
    
  7. Gabe Hollombe   May 18, 2008 @ 01:54 PM

    String.gsub is great but String.delete is also helpful here, and perhaps more readable.

    For any of the gsub examples above, you could use delete instead, like this:
    miles = miles.delete(',')
    will work.
  8. Brandon Beacher   May 18, 2008 @ 05:54 PM

    Chris Lloyd is closest to my preferred solution, but I normalize prior to the validation, rather than the save.

    class Vehicle &lt; ActiveRecord::Base
      before_validation :normalize_miles
    
      validates_format_of :miles, :with =&gt; /\d*/
    
    private
    
      def normalize_miles
        self.miles = miles.delete(&quot;^0-9&quot;)
      end
    
    end
    
  9. labria   May 18, 2008 @ 09:43 PM

    I’m too tired to type actual code, but i would check if the data is ONLY numbers and commas. If so, I would strip the commas out, make an int, and be happy. If not, i would alert the user that he entered something wrong.

  10. Chris   May 19, 2008 @ 01:00 AM

    If you strip out decimals, then 1005.5 becomes 10055. Arron had it right by stripping out the comma but then doing to_i to get rid of the decimal. However, the code should probably go in the model instead of the controller.

  11. matt m   May 19, 2008 @ 01:08 AM

    if you’re in JRuby, NumberFormat offers a nice locale sensitive solutions (where commas and dots have the appropriate meaning in places where the comma is the decimal point equivalent).

    Of course, it’s far from elegant!!!
    try {
      NumberFormat  nf = NumberFormat.getNumberInstance();
      Number n = nf.parse(miles); 
      //note that getting that miles string isn't as easy as it looks
    }
    catch (ParseException p) {
      //this bit might get tricky
    }
    
  12. Sur   May 19, 2008 @ 03:00 AM

    I think, the very first thing should be to give a kick within the client side UI itself, why to put the “MUST” extra work on the server. More or less whatever mentioned above is a tweak or FIX we are doing for the value that user has entered without letting the user know that there might be some tweaking going to happen with the data he entered. Of course there should be such a code in the model but things should also be handled within the browser using JS.

    In my rails applications I always handle such things with a simple function by capturing the onKeyUp event on the text field. And the functionality should also work on everything apart from 0-9 if we are considering the integer, not only the commas but any other character but numeral.

    So, the best here, and what I do is two things…

    Client Side JS code

    specifying in the text_field or text_field_tag options as
     :onkeyup => "check_integer(this)" 
    
    adding the JS function
     function check_integer(el)
     {
      if(String(parseInt(el.value))=="NaN")
        el.value = '';
      else
        el.value = parseInt(el.value);
     }
    

    This JS function will automatically remove any character other than 0-9 and will left a valid numeric value in the text field while the user is typing in the box.

    Server side Ruby code

    definitely I will go with the more generic “gsub” to swipe out anything other than 0-9

     def normalize_miles
       self.miles.gsub!(/[^0-9]/, '')
     end
    
  13. Andrey   May 19, 2008 @ 06:52 AM

    Stupidity is the property a person, action or belief instantiates by virtue of having or being indicative of low intelligence or poor learning abilities.

  14. Markús   May 19, 2008 @ 05:00 PM

    I came into a problem like this in a recent project. There was a model called Sale which had a price, represented by a number without decimals and using dots instead of commans for hundred separation (used in Spain). The model looked like this:

    class Sale < ActiveRecord::Base
      before_validation :remove_price_dots
    
      validates_presence_of :price
      validates_numericality_of :price
    
      def remove_price_dots
        self.price = price.delete(".")
        # I don't want to stripe out all non-number characters with grep(/[^\d]/, '') because
        # the user might be typing 'I walked 5 miles from home' and it would be valid
      end
    
      def price
        price.to_i.to_s.reverse.split('').in_groups_of(3,false).collect{|g| g.to_s}.join('.').reverse
        # for an amount of 50000 it will show 50.000, this will be used in update forms as well
        # It might exists another simpler solution for this
      end
    end
    
  15. Joe F.   May 20, 2008 @ 04:06 PM

    I wrote a plugin to accomplish this same task for currency fields, where an amount needs to be entered in dollars. Users will often use a ’$’ to enter the number. The code can be found here:

    http://github.com/faithfulgeek/acts_as_currency/tree

  16. Sam   May 26, 2008 @ 08:14 AM

    Just a complement on this post Jeff…I get so disappointed when I run into user interfaces like the horrid one you showed us.

  17. Ellroy   May 30, 2008 @ 11:17 PM

    As is shown in the writings of Galileo, our ideas stand in need to general logic.