Problematic Decimal Arithmetic in Javascript

by Dean

It is a pretty well known fact that using javascript to add decimals 0.1 with 0.2 does not result in 0.3. [1] Try it yourself with the FireBug console. For the uninitiated, the problem stems from javascript’s internal representation of numbers. They are actually binary numbers that are usually exact, but sometimes for example, are 0.00000000000000004 off. This is particularly aggravating when writing calculators that rely on js to give accurate results.

In my text inputs I was using toFixed() and some magic HTML attributes to keep decimals nice and clean. However, this method breaks down when a user enters a number with more significant figures than initially set up or you try to operate on two numbers with different sig figs. It was probably inevitable that I use a little arithmetic library extending Number to make decimals play nice.

Since javascript is 13 years old I thought it would be a simple thing to find such a library. I was wrong. After four days learning, looking and lamenting I had no library. After putting this one together in about a day and a half I am not surprised that nobody published theirs. Most of the eleven functions are one-liners, yet it bothers me that there are probably thirty-odd implementations of the this out there and not one found through Google.

  1. // decimal_arithmetic.js
  2. String.prototype.digitsAfterDecimal = function()
  3. {  var parts = this.split(".", 2);  // FIXME: Not international!
  4.    if( ! parts[1] )
  5.    {  parts[1] = "";  }
  6.    return parts[1].length;
  7. };
  8.  
  9. Number.prototype.biggerScalar = function(n)
  10. {  return n.scale() > this.scale() ? n.scale() : this.scale();  };
  11.  
  12. Number.prototype.digitsAfterDecimal = function()
  13. {  return this.toString().digitsAfterDecimal();  };
  14.  
  15. Number.prototype.divided = function(n)
  16. {  return this.dividedBy(n);  };
  17.  
  18. Number.prototype.dividedBy = function(n)
  19. {  return this.multiply( n.reciprocal() );  };
  20.  
  21. Number.prototype.minus = function(n)
  22. {  return this.plus( n.negative() );  };
  23.  
  24. Number.prototype.multiply = function(n)
  25. {  var s = this.biggerScalar(n);
  26.    return (Math.round(s*this,0) * Math.round(s*n,0)) / (s*s);
  27. };
  28.  
  29. Number.prototype.negative = function()
  30. {  return -1 * this;  };
  31.  
  32. Number.prototype.plus = function(n)
  33. {  var s = this.biggerScalar(n);
  34.    return (Math.round(s*this,0) + Math.round(s*n,0)) / s;
  35. };
  36.  
  37. Number.prototype.reciprocal = function()
  38. {  return 1 / this;  };
  39.  
  40. Number.prototype.scale = function()
  41. {  return Math.pow(10, this.digitsAfterDecimal() );  };
  42.  

Now you can do magical things like:

  1. 0.1.plus(0.2)
  2. // 0.3

yielding the correct results.

I am looking forward to javascript 2.0 when I can override the + operator. Maybe I won’t go that far since binary arithmetic is still faster.

[1] http://groups.google.com/group/comp.lang.javascript/…

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Posted by: Dean @ 10:09 pm

Categories: Dean, code, javascript |

2 Comments »

 Extending acts_as_commentable

by Dean

acts_as_commentable is a nice little ruby on rails plugin. It extends your ActiveRecord classes giving them comments. We are going to use comments on all kinds of things, starting with recipes, of course. However, AAC lacks a critical feature: the ability for users to approve comments before they are displayed. In this post I am going to run through extending AAC using acts_as_state_machine.

The first thing I did (and do to all the plugins we use) was pistonize the plugin so I could hack on it without fear of getting my changes destroyed.

I start off simply here by adding two states to the Comment model: :pending and :approved.

  1. class Comment < ActiveRecord::Base
  2.   # The first element of this array is the initial state
  3.   VALID_STATES = [ :pending, :approved ]
  4.   acts_as_state_machine :initial => VALID_STATES[0]
  5.  
  6.   event :approve do
  7.     transitions :from => :pending, :to => :approved
  8.   end
  9.  
  10.   VALID_STATES.each do |_state|
  11.     # Define _state as a state
  12.     state _state
  13.   end
  14.  
  15.   # More code snipped
  16. end

Now we are going to write some real code, so here comes a little RSpec. aac provides three class methods:

  1. class Comment < ActiveRecord::Base
  2.   class << self
  3.   # Helper class method to lookup all comments assigned
  4.   # to all commentable types for a given user.
  5.   def find_comments_by_user(user)
  6.  
  7.   # Helper class method to look up all comments for
  8.   # commentable class name and commentable id.
  9.   def find_comments_for_commentable(commentable_str, commentable_id)
  10.  
  11.   # Helper class method to look up a commentable object
  12.   # given the commentable class name and id
  13.   def find_commentable(commentable_str, commentable_id)
  14.   end

Since it didn’t come with Test::Unit or RSpec tests I wrote up some test for these methods.

  1. describe Comment, "class methods" do
  2.   fixtures :comments, :recipes, :users
  3.   it "should find comments by user" do
  4.     Comment.find_comments_by_user( comments(:comment_one).user ).should all_belong_to( comments(:comment_one).user )
  5.   end
  6.  
  7.   # This could be more specific
  8.   it "should find comments for a particular class" do
  9.     Comment.find_comments_for_commentable( Comments(:comment_one).commentable_type, comments(:comment_one).commentable_id ).should be_an_instance_of(Array)
  10.   end
  11.  
  12.   it "should find all comments for a particular class" do
  13.     # I happen to know that comment_one is a recipe comment
  14.     Comment.find_commentable( "Recipe", comments(:comment_one).commentable_id ).should be_an_instance_of(Recipe)
  15.   end
  16.  
  17. end

If you are confused by should all_belong_to then you should check out my previous post. With these specs out of the way we can go on to adding more new code.

  1.  it "should find approved comments by user" do
  2.     Comment.find_approved_comments_by_user( comments(:comment_one).user ).should all_be_in_state("approved")
  3.   end
  4.  
  5.   it "should find pending comments by user" do
  6.     Comment.find_pending_comments_by_user( comments(:comment_one).user ).should all_be_in_state("pending")
  7.   end
  8. end

Now, normally you would write one spec at a time, but I think I would bore my readers, so I combined these two. Also take note that I am using another custom RSpec matcher all_be_in_state(). It looks a lot like all_belong_to(), so I leave its implementation as an exercise to the reader (unless I can get another blog post out of it). To get these tests to pass I add a few lines of code:

  1.  VALID_STATES.each do |_state|
  2.     # Define _state as a state
  3.     state _state
  4.  
  5.     # Add Comment.find__comments methods
  6.     ( class << self; self; end ).instance_eval do
  7.       define_method "find_#{_state}_comments_by_user" do |_user|
  8.         find_in_state( :all, _state, :conditions => ["user_id = ?", _user.id], :order => "created_at DESC" )
  9.       end
  10.     end
  11.   end

I am not a method_missing kind of guy, and prefer the dynamic-method metaprogramming style. This lot of code defines class methods at runtime that find Comments in specific states. I am actually using whytheluckystiff’s metaid to hide some of the meta-junk, but I thought I should spell it out here for clarity.

Well, now we have a Comment class with two states and code to limit finds to cmments in a specific state. Right now, that is all I have. Here is the full code for the Comment class and the RSpec. You will see another custom RSpec matcher here, require_a().

  1. class Comment < ActiveRecord::Base
  2.  
  3.   # The first element of this array is the initial state
  4.   VALID_STATES = [ :pending, :approved ]
  5.  
  6.   acts_as_state_machine :initial => VALID_STATES[0]
  7.  
  8.   belongs_to :commentable, :polymorphic => true
  9.   belongs_to :user
  10.  
  11.   event :approve do
  12.     transitions :from => :pending, :to => :approved
  13.   end
  14.  
  15.   validates_associated :user
  16.   validates_presence_of :comment, :commentable_id, :commentable_type, :state,                           :user_id
  17.  
  18.   VALID_STATES.each do |_state|
  19.     # Define _state as a state
  20.     state _state
  21.  
  22.     # Add Comment.find_<state>_comments methods
  23.     meta_def "find_#{_state}_comments_by_user" do |_user|
  24.       find_in_state( :all, _state, :conditions => ["user_id = ?", _user.id],
  25.                      :order => "created_at DESC" )
  26.     end
  27.   end
  28.  
  29.   class < < self
  30.  
  31.     # Helper class method to look up a commentable object
  32.     # given the commentable class name and id
  33.     def find_commentable(commentable_str, commentable_id)
  34.       commentable_str.constantize.find(commentable_id)
  35.     end
  36.  
  37.     # This could be refactored into find_<state>_comments_by_user (somehow)
  38.     def find_comments_by_user(_user)
  39.       find( :all, :conditions => ["user_id = ?", _user.id],
  40.             :order => "created_at DESC" )
  41.     end
  42.  
  43.     # Helper class method to look up all comments for
  44.     # commentable class name and commentable id.
  45.     def find_comments_for_commentable(commentable_str, commentable_id)
  46.       find( :all,
  47.             :conditions => [ "commentable_type = ? and commentable_id = ?",
  48.                              commentable_str, commentable_id ],
  49.             :order => "created_at DESC" )
  50.     end
  51.  
  52.   end
  53.  
  54. end</state>
  1. require File.dirname(__FILE__) + ‘/../../../../spec/spec_helper’
  2.  
  3. module CommentSpecHelper
  4.  
  5. end
  6.  
  7. describe Comment do
  8.  
  9.   fixtures :comments
  10.  
  11.   include CommentSpecHelper
  12.  
  13.   before(:each) do
  14.     @comment = Comment.new
  15.   end
  16.  
  17.   it "should start out in pending state" do
  18.     @comment.state.should == "pending"
  19.   end
  20.  
  21.   it "sould transition to approved" do
  22.     @comment = comments(:pending_comment)
  23.     @comment.approve!
  24.     @comment.state.should == "approved"
  25.   end
  26.  
  27.   it "should require a comment" do
  28.     @comment.should require_a(:comment)
  29.   end
  30.  
  31.   it "should require a commentable_id" do
  32.     @comment.should require_a(:commentable_id)
  33.   end
  34.  
  35.   it "should require a commentable_type" do
  36.     @comment.should require_a(:commentable_type)
  37.   end
  38.  
  39.   it "should require a state" do
  40.     @comment.should require_a(:state)
  41.   end
  42.  
  43.   it "should require a user_id" do
  44.     @comment.should require_a(:user_id)
  45.   end
  46.  
  47. end
  48.  
  49. describe Comment, "class methods" do
  50.  
  51.   fixtures :comments, :recipes, :users
  52.  
  53.   it "should find all comments for a particular class" do
  54.     # I happen to know that comment_one is a recipe comment
  55.     Comment.find_commentable( "Recipe", comments(:comment_one).commentable_id ).should be_an_instance_of(Recipe)
  56.   end
  57.  
  58.   it "should find comments by user" do
  59.     Comment.find_comments_by_user( comments(:comment_one).user ).should all_belong_to( comments(:comment_one).user )
  60.   end
  61.  
  62.   it "should find approved comments by user" do
  63.     Comment.find_approved_comments_by_user( comments(:comment_one).user ).should all_be_in_state("approved")
  64.   end
  65.  
  66.   it "should find pending comments by user" do
  67.     Comment.find_pending_comments_by_user( comments(:comment_one).user ).should all_be_in_state("pending")
  68.   end
  69.  
  70.   # This could be more specific
  71.   it "should find comments for a particular class" do
  72.     Comment.find_comments_for_commentable( comments(:comment_one).commentable_type, comments(:comment_one).commentable_id ).should be_an_instance_of(Array)
  73.   end
  74.  
  75. end

–Dean

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Posted by: Dean @ 8:48 pm

Categories: Dean, code, ruby |

No Comments »

 belong_to RSpec matcher

by Dean

I was extending acts_as_commentable and needed a good RSpec test to check the returned objects from its finder methods belonged to the correct user. For example, Comment.find_comments_by_user( :some_user ) should all belong_to :some_user. I’ll be darned if that doesn’t look like a RSpec description. Since there is no all_belong_to matcher, I wrote one.

  1. module ActiveRecordValidations
  2.   class BelongTo
  3.     def initialize(expected)
  4.       @expected = expected
  5.     end
  6.  
  7.     def matches?(args)
  8.       args.all? do |target|
  9.         @target = target
  10.         @target.send(@expected.class.to_s.downcase) == @expected
  11.       end
  12.     end
  13.  
  14.     def failure_message
  15.       "expected #{@target.inspect} to all belong to #{@expected}"
  16.     end
  17.  
  18.     def negative_failure_message
  19.       "expected #{@target.inspect} not to all belong to #{@expected}"
  20.     end
  21.   end
  22.  
  23.   def belong_to(expected)
  24.     BelongTo.new( [expected] )
  25.   end
  26.  
  27.   def all_belong_to(expected)
  28.     BelongTo.new( expected )
  29.   end
  30. end

The matches? method takes an array of objects and goes through them with all? checking that they have a belongs_to the expected thing. Using the example above, each comment object returned by Comment.find_comments_by_user would get tested if comment.user== @user.

–Dean

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Posted by: Dean @ 4:59 pm

Categories: Dean, code, ruby |

No Comments »

 BJCP Beer Style Guidelines Updated for 2008

by gregr

Last month the Beer Judges Certification Program (BJCP) released a minor update to their 2004 Style Guidelines. The new “2008 update to the 2004 style guidelines” (as it is officially called) includes some updated style parameters, changes to style descriptions, revised commericial examples for most beer styles and some significant rework to many Belgian ale styles.

In BrewSession we’re using the XML format of the BJCP style guidelines. So as soon as I saw that the 2008 updates had been released, I wanted to make sure we got those into BrewSession as soon as possible. I contacted the BJCP and found that no one was working on updating the BJCP XML. I attempted to contact the original author of the BJCP XML without success (Chris, if you’re out there, give us a hollar, I have some questions). So, I volunteered to update the BJCP XML to the 2008 version.

The BJCP describes it as a minor update, and while there aren’t any new beer styles in the 2008 update, or high level category changes, the number of revisions made to the “fine print” are fairly numerous. While tedious, comparing the 2008 styles with the 2004 XML and making the needed changes is quite interesting, as I’m getting to know the styles like I never did before. In fact, I started going through the styles on my own, buying 2-3 representatives of each style, taking some sips, and reflecting on the style guidelines. My own BJCP course! Dean’s already a certified BJCP judge, but I’m not. However, I think at the end of this I’ll be prepared to take the exam.

In addition to updating the text of the styles, I’m recommending that some elements of the XML be changed, enhanced and updated to make the BJCP Styles XML document more useful to a wider range of people and applications. I’ll be posting my proposals here shortly…

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Posted by: gregr @ 5:35 pm

Categories: Greg, code, features, related |

No Comments »

 Concise Signup & Signin Pages

by Dean

Login, Signup

We are presented with these quick forms all the time. While it is easy to create standard login and signup pages, Amazon.com has a good one:

Amazon Sigin Image

What makes it good?

First of all, the prompts are written in plain English. Amazon sells to a wide slice of the population, meaning that about 15% of their customers are probably not very technology-saavy. (-2?) Anything they can do to ease the operation helps their customers buy.

Secondly, when a user visits Amazon.com, there is only one link: “Your account” instead of separate login and signup links. Simple is generally better.

Also note the standard “forgot your password” link plus an additional “has your email address changed?” question. Both are useful to have close at hand.

Implementation

Rails 2.0 strongly encourages you to design RESTful applications. Login forms are associated with Session objects, while signup forms go with User objects (rather, Brewer objects in our case). A simple redirect in the SessionsController#create method takes care of pointing a user in the right direction.

  1.  
  2. class SessionsController < ApplicationController
  3.   def create
  4.     if params[:signin_action] == ‘new_user’
  5.       redirect_to new_brewer_path( :brewer => {:email => params[:email]} )
  6.     else
  7.       # Do sigin stuff
  8.     end
  9.   end
  10. end

Note that we pass params[:email] to the new_brewer_path so that field is automatically populated on the next page. If you are using the generated scaffold, you will have to change your BrewersController#new method to instantiate a new @brewer object:

  1.  
  2. class BrewersController < ApplicationController
  3.   def new
  4.     @brewer = Brewer.new(params[:brewer])
  5.   end
  6. end

Lastly, here is the extra test:

  1.  
  2. class SessionsControllerTest < Test::Unit::TestCase
  3.   def test_should_redirect_to_new_brewer_if_asked
  4.     an_email = "dean@brewsession.com"
  5.     post :create, :email => an_email, :signin_action => ‘new_user’
  6.     assert_redirected_to new_brewer_path(:brewer => {:email => an_email} )
  7.     assert_nil session[:brewer_id]
  8.   end
  9. end

In a later post I will talk about how to implement the change password action in a RESTful way.

–Dean

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Posted by: Dean @ 10:16 am

Categories: Dean, code, ruby |

No Comments »

 Could be an Obstacle

by Dean

Hi Reader,

I am going to tell you the truth. I was not looking forward to writing the javascript necessary to get BrewSession going. For one thing I am a stickler for strict separation of behavior and content. Cascading Style Sheets are great because they keep the page content from getting lost in the necessary formatting to make a pretty-looking page - there is no equivalent for keeping javascript from obfuscating the stuff on your page.

I have a working prototype for keeping javascript off the page, but I feel like it is just one more thing I need to finish before I can get to the real programming. MochiKit is nice to program with, but laying the foundation for a cool sparkley web site was dragging on.

Enter the Google Web Toolkit. It “…is an open source Java software development framework that makes writing AJAX applications … easy for developers who don’t speak browser quirks as a second language.” Using swing-like widgets and panels, you can quickly put together a functioning app. I do not know swing or even much java, however that is not the obstacle to which I alluded in this post’s title.

I’ve been steaming out a mini-app for … let’s call them a client. It’s quite easy and I’m pleased with the toolkit. I can already see the pieces working together for BrewSession. Now round-about to the problem. It is not currently well-integrated with Rails.

Allow me to geek out on you. Rails is an MVC architecture with great M and C tools and decent V tools. The community believes in doing it yourself, leaning towards being elitist about it. I already have a great start to developing BrewSession into a full-fledged REST service thanks to Rails. I still love working with ruby and am going to keep rails on the server side. All I need to do is get BrewSession, written in GWT, to consume the REST service.

Searching the google group for the toolkit brings up a whole lotta nothing, so it looks like I have to do it myself. The way things are going I will someday refer to myself as a programmer rather than a sys admin.

–Dean

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Posted by: Dean @ 10:58 pm

Categories: Dean, code |

No Comments »

 Sneek Peeks

by gregr

As Dean mentioned below, we’re working on some calculators that we’ll release as we complete them, since they’re still useful without the rest of the software.

I haven’t posted here yet … so my quick introduction. I’m Greg. I tend to GUI design elements, the web site, brewing calculations, ingredients databases, and a few other things. Of course there’s overlap in what we both do. ;-)

Though we can’t make any promises, we’re hoping to release the following calculators in the coming month or two:

1. Carbonation/Draft System Balancer
2. Calorie/Carb Counter
3. Yeast Pitching Rate
4. Kettle Volume
5. Dilution Calculator

Not necessarily in that order, but… To us, those look like a good cross section of functions to just get out there and start having people play with.

UPDATE (May 20) - The Calorie/Carb Counter, Dilution and Kettle volume calculators are nearing completion.  Keep on the look out!

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Posted by: gregr @ 9:13 am

Categories: Greg, code, features |

No Comments »

 Hey, my first rails plugin

by Dean

Ruby on Rails allows for plugins to extend its functionality. My first attempt at one is nothing special but I am going to blog about it anyway :-D

I needed a way to check if a number is within a range before saving a record to the database. Rails has a number of good validations built in, but not one that does that. In two or three hours, I was able to piece together a simple plugin that adds “validates_range_of” to my AR models. The code is a little ugly until I can get Greg to install a syntax highlighting plugin for this blog.

  1. module ActiveRecord
  2. module Validations
  3. module ClassMethods
  4. def validates_range_of(*attrs)
  5. options = {  :on => :save  }
  6. options.update(attrs.pop) if attrs.last.is_a?(Hash)
  7. attrs.flatten!
  8. unless options[:message]
  9. if options[:maximum] && options[:minimum]
  10. options[:message] = ‘must be betweeen ‘ + options[:minimum].to_s + ‘ and ‘ + options[:maximum].to_s
  11. elsif options[:maximum]
  12. options[:message] = ‘must be less than or equal to ‘ + options[:maximum].to_s
  13. elsif options[:minimum]
  14. options[:message] = ‘must be greater than or equal to ‘ + options[:minimum].to_s
  15. else
  16. raise ArgumentError, ‘Range unspecified.  Specify the :maximum and/or :minimum.’
  17. end
  18. end
  19.  
  20. validates_numericality_of( attrs, options )
  21.  
  22. validates_each(attrs, options) do |record, attr_name, value|
  23. if ((!options[:maximum].nil?) && (!options[:minimum].nil?)) &&
  24. (value > options[:maximum] || value < options[:minimum])
  25. record.errors.add( attr_name, options[:message] )
  26. elsif (!options[:maximum].nil?) && value > options[:maximum]
  27. record.errors.add( attr_name, options[:message] )
  28. elsif (!options[:minimum].nil?) && value < options[:minimum]
  29. record.errors.add( attr_name, options[:message] )
  30. end
  31. end
  32. end
  33. end
  34. end
  35. end

As you can see this plugin extends AR::Validations. So my models can use validates_range_of to check minimum and maximum values. Let’s look at the BJCP Scoresheet I needed it for:

  1. class BjcpScoresheet < ActiveRecord::Base
  2. belongs_to :brew_session
  3. validates_associated :brew_session
  4. validates_range_of :brew_session_id, { :minimum => 1, :message => ‘does not belong to a brew session’ }
  5. validates_range_of :aroma_score, { :minimum => 1, :maximum => 12, :only_integer => true }
  6. validates_range_of :appearance_score, { :minimum => 1, :maximum => 3, :only_integer => true }
  7. validates_range_of :flavor_score, { :minimum => 1, :maximum => 20, :only_integer => true }
  8. validates_range_of :mouthfeel_score, { :minimum => 1, :maximum => 10, :only_integer => true }
  9. validates_range_of :impression_score, { :minimum => 1, :maximum => 10, :only_integer => true }

How nice - reusable user input validation.

–Dean

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Posted by: Dean @ 1:46 am

Categories: Dean, code, ruby |

2 Comments »

 Of all the things I miss….

by Dean

Before the death of my drives I had a flexible Measurement class written up. As most of you know brewing involves all kinds of measurement - hop weights, boil volume, bitterness, etc. The class served as a base that more specific classes would inherit and provided the framework for converting between different measurement systems. For example, the SpecificGravity class would inherit Measurement and provide the conversion code to switch between SG and Plato.

The database would save the scalar and units and instantiate an aggregation of these two pieces. When the units changed, back into the database it went with the new values. It was very easy to work with, but took me quite a bit of time to write.

“Why not use one of the libraries already available?” you might ask. The short answer is that they do not fit my needs. Firstly, none of them produced objects that I could stuff in the database as a aggregation - most are intended as great extensions to various number classes. Secondly, and most importantly, none of them dealt with Bitterness, Color or Specific Gravity, which is really why I need a measurement class. Lastly, most of them only handled linear transformations:

m2 = a * m1 + b

To convert from SG to Plato you need to use a cubic polynomial, ugh.

So I wrote my own, then re-wrote it, and it was beautiful. Now I am re-engineering the whole thing. In addition, I’ll have to figure out where I got the Plato to SG reverse conversion. I remember trying to solve the cubic equation, then finding something that actually worked.

It is a little faster-going because I wrote it all before, but I had some tr1cky 31337 code in there that I will have to figure out again.

Always make sure your backups are working and current.

–Dean

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Posted by: Dean @ 9:51 pm

Categories: Dean, code, disaster, ruby |

1 Comment »

 It doesn’t have to start out so advanced

by Dean

Merry Christmas. Here’s a little javascript brewsession present.

http://ror.deanandadie.net/table.html

I was thinking about the ingredients table Greg designed. While it is good looking and we’ll definitely use it, I would rather see a very simple list to start with and give the option to have an “advanced view”. It occurred to me to let the user choose which columns they want to see. So I restarted work on BrewSession again with this prototype. It’s a work in progress, but a solid start.

–Dean

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Posted by: Dean @ 8:21 am

Categories: Dean, code, features, javascript |

No Comments »

Our Sponsors