├── .rspec ├── Gemfile ├── .rvmrc ├── .document ├── lib ├── acts_as_decimal │ ├── version.rb │ └── acts_as_decimal.rb └── acts_as_decimal.rb ├── .travis.yml ├── .gitignore ├── spec ├── spec_helper.rb ├── model_builder.rb └── acts_as_decimal_spec.rb ├── LICENSE ├── Rakefile ├── acts_as_decimal.gemspec ├── Readme.md └── Gemfile.lock /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm --create use ruby-1.9.2@acts_as_decimal > /dev/null 2 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /lib/acts_as_decimal/version.rb: -------------------------------------------------------------------------------- 1 | module ActsAsDecimal 2 | # The version number. 3 | VERSION = "1.1.1" 4 | end 5 | -------------------------------------------------------------------------------- /lib/acts_as_decimal.rb: -------------------------------------------------------------------------------- 1 | require 'acts_as_decimal/acts_as_decimal' 2 | require 'active_record' 3 | 4 | ActiveRecord::Base.send :include, ActsAsDecimal 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # http://about.travis-ci.org/docs/user/build-configuration/ 2 | rvm: 3 | - 1.8.7 4 | - 1.9.2 5 | - ree 6 | - ruby-head 7 | - rbx 8 | - rbx-2.0 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | .bundle/ 21 | 22 | ## PROJECT::SPECIFIC 23 | *.rbc 24 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start do 3 | add_group "Lib", "lib" 4 | end 5 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 6 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 7 | 8 | require 'acts_as_decimal' 9 | require 'active_record' 10 | require 'rspec' 11 | ActiveSupport::Deprecation.silenced = true 12 | 13 | ActiveRecord::Base.establish_connection( 14 | :adapter => 'sqlite3', 15 | :database => ':memory:' 16 | ) 17 | 18 | ActiveRecord::Schema.define do 19 | create_table :products do |t| 20 | t.string :name 21 | t.integer :price 22 | end 23 | end 24 | 25 | class Product < ActiveRecord::Base 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Codegram 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /spec/model_builder.rb: -------------------------------------------------------------------------------- 1 | # This is based on Remarkable based on Shoulda model builder for Test::Unit. 2 | # 3 | module ModelBuilder 4 | 5 | def create_table(table_name, &block) 6 | connection = ActiveRecord::Base.connection 7 | 8 | connection.execute("DROP TABLE IF EXISTS #{table_name}") 9 | connection.create_table(table_name, &block) 10 | @created_tables ||= [] 11 | @created_tables << table_name 12 | connection 13 | end 14 | 15 | def define_constant(class_name, base, &block) 16 | class_name = class_name.to_s.camelize 17 | 18 | klass = Class.new(base) 19 | Object.const_set(class_name, klass) 20 | 21 | klass.class_eval(&block) if block_given? 22 | 23 | @defined_constants ||= [] 24 | @defined_constants << class_name 25 | 26 | klass 27 | end 28 | 29 | def define_model_class(class_name, &block) 30 | define_constant(class_name, ActiveRecord::Base, &block) 31 | end 32 | 33 | def define_model(name, columns = {}, &block) 34 | class_name = name.to_s.pluralize.classify 35 | table_name = class_name.tableize 36 | 37 | table = columns.delete(:table) || lambda {|table| 38 | columns.each do |name, type| 39 | table.column name, *type 40 | end 41 | } 42 | 43 | create_table(table_name, &table) 44 | 45 | klass = define_model_class(class_name, &block) 46 | instance = klass.new 47 | 48 | self.class.subject { instance } if self.class.respond_to?(:subject) 49 | instance 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core/rake_task' 5 | desc "Run acts_as_decimal specs" 6 | RSpec::Core::RakeTask.new 7 | 8 | require 'yard' 9 | YARD::Rake::YardocTask.new(:docs) do |t| 10 | t.files = ['lib/**/*.rb'] 11 | t.options = ['-m', 'markdown', '--no-private', '-r', 'Readme.md', '--title', 'Acts as Decimal documentation'] 12 | end 13 | 14 | site = 'doc' 15 | source_branch = 'master' 16 | deploy_branch = 'gh-pages' 17 | 18 | desc "generate and deploy documentation website to github pages" 19 | multitask :pages do 20 | puts ">>> Deploying #{deploy_branch} branch to Github Pages <<<" 21 | require 'git' 22 | repo = Git.open('.') 23 | puts "\n>>> Checking out #{deploy_branch} branch <<<\n" 24 | repo.branch("#{deploy_branch}").checkout 25 | (Dir["*"] - [site]).each { |f| rm_rf(f) } 26 | Dir["#{site}/*"].each {|f| mv(f, "./")} 27 | rm_rf(site) 28 | puts "\n>>> Moving generated site files <<<\n" 29 | Dir["**/*"].each {|f| repo.add(f) } 30 | repo.status.deleted.each {|f, s| repo.remove(f)} 31 | puts "\n>>> Commiting: Site updated at #{Time.now.utc} <<<\n" 32 | message = ENV["MESSAGE"] || "Site updated at #{Time.now.utc}" 33 | repo.commit(message) 34 | puts "\n>>> Pushing generated site to #{deploy_branch} branch <<<\n" 35 | repo.push 36 | puts "\n>>> Github Pages deploy complete <<<\n" 37 | repo.branch("#{source_branch}").checkout 38 | end 39 | 40 | task :doc => [:docs] 41 | 42 | task :default => :spec 43 | task :test => [:spec] 44 | -------------------------------------------------------------------------------- /acts_as_decimal.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | require "acts_as_decimal/version" 3 | 4 | Gem::Specification.new do |s| 5 | s.name = %q{acts_as_decimal} 6 | s.version = ActsAsDecimal::VERSION 7 | s.platform = Gem::Platform::RUBY 8 | s.authors = ["Oriol Gual", "Josep M. Bach", "Josep Jaume Rey"] 9 | s.email = %q{info@codegram.com} 10 | s.homepage = %q{http://github.com/codegram/acts_as_decimal} 11 | s.summary = %q{Treat an attribute as if it were a decimal, storing it as an integer in the database.} 12 | s.description = %q{Rails 3 gem to treat an attribute as a decimal (getting and setting floating-point values) but storing it as an integer in the database (useful for prices and other precision-needed attributes like money).} 13 | s.rubyforge_project = 'acts_as_decimal' 14 | 15 | s.add_runtime_dependency 'activemodel', '>= 3.0.0' 16 | s.add_runtime_dependency 'activesupport', '>= 3.0.0' 17 | s.add_runtime_dependency 'actionpack', '>= 3.0.0' 18 | 19 | s.add_development_dependency 'rspec', '>= 2.5.0' 20 | s.add_development_dependency 'activerecord', '>= 3.0.0' 21 | s.add_development_dependency 'sqlite3' 22 | s.add_development_dependency 'rake' 23 | 24 | s.add_development_dependency 'simplecov' 25 | s.add_development_dependency 'yard' 26 | s.add_development_dependency 'bluecloth' 27 | 28 | s.files = `git ls-files`.split("\n") 29 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 30 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 31 | s.require_paths = ["lib"] 32 | end 33 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # acts_as_decimal 2 | 3 | A simple gem for Rails 3 to make an attribute behave like it is floating point, 4 | being stored as an integer in the database. Tested with Ruby 1.8.7, 1.9.2, Rubinius 2.0.0pre and JRuby 1.6.2. 5 | 6 | Add it to your Gemfile: 7 | 8 | ````ruby 9 | gem 'acts_as_decimal' 10 | ```` 11 | 12 | And put this in your model, let's say a Product with a `#price` attribute: 13 | 14 | ````ruby 15 | class Product < ActiveRecord::Base 16 | acts_as_decimal :price # Defaults to 2 decimal floating point values, or... 17 | acts_as_decimal :price, :decimals => 5 # ...as you wish! 18 | end 19 | ```` 20 | 21 | Now you store and retrieve `#price` as a floating point: 22 | 23 | ````ruby 24 | product = Product.new 25 | product.price = 12.30 26 | product.price # => 12.30 27 | ```` 28 | 29 | But you still have access to the raw database integer value through `#price_raw`: 30 | 31 | ````ruby 32 | product.price_raw # => 1230 33 | product.price_raw = 4309 # product.price == 43.09 34 | ```` 35 | 36 | ## Note on Patches/Pull Requests 37 | 38 | * Fork the project. 39 | * Make your feature addition or bug fix. 40 | * Add tests for it. This is important so I don't break it in a future version unintentionally. 41 | * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 42 | * Send me a pull request. Bonus points for topic branches. 43 | 44 | ## Copyright 45 | 46 | Copyright (c) 2010 Codegram. See LICENSE for details. 47 | 48 | 49 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | acts_as_decimal (1.1.1) 5 | actionpack (>= 3.0.0) 6 | activemodel (>= 3.0.0) 7 | activesupport (>= 3.0.0) 8 | 9 | GEM 10 | remote: http://rubygems.org/ 11 | specs: 12 | abstract (1.0.0) 13 | actionpack (3.0.0) 14 | activemodel (= 3.0.0) 15 | activesupport (= 3.0.0) 16 | builder (~> 2.1.2) 17 | erubis (~> 2.6.6) 18 | i18n (~> 0.4.1) 19 | rack (~> 1.2.1) 20 | rack-mount (~> 0.6.12) 21 | rack-test (~> 0.5.4) 22 | tzinfo (~> 0.3.23) 23 | activemodel (3.0.0) 24 | activesupport (= 3.0.0) 25 | builder (~> 2.1.2) 26 | i18n (~> 0.4.1) 27 | activerecord (3.0.0) 28 | activemodel (= 3.0.0) 29 | activesupport (= 3.0.0) 30 | arel (~> 1.0.0) 31 | tzinfo (~> 0.3.23) 32 | activesupport (3.0.0) 33 | arel (1.0.1) 34 | activesupport (~> 3.0.0) 35 | bluecloth (2.0.11) 36 | builder (2.1.2) 37 | diff-lcs (1.1.2) 38 | erubis (2.6.6) 39 | abstract (>= 1.0.0) 40 | i18n (0.4.1) 41 | rack (1.2.3) 42 | rack-mount (0.6.14) 43 | rack (>= 1.0.0) 44 | rack-test (0.5.7) 45 | rack (>= 1.0) 46 | rake (0.9.2) 47 | rspec (2.5.0) 48 | rspec-core (~> 2.5.0) 49 | rspec-expectations (~> 2.5.0) 50 | rspec-mocks (~> 2.5.0) 51 | rspec-core (2.5.1) 52 | rspec-expectations (2.5.0) 53 | diff-lcs (~> 1.1.2) 54 | rspec-mocks (2.5.0) 55 | simplecov (0.4.1) 56 | simplecov-html (~> 0.4.3) 57 | simplecov-html (0.4.3) 58 | sqlite3 (1.3.3) 59 | tzinfo (0.3.23) 60 | yard (0.6.4) 61 | 62 | PLATFORMS 63 | ruby 64 | 65 | DEPENDENCIES 66 | activerecord (>= 3.0.0) 67 | acts_as_decimal! 68 | bluecloth 69 | rake 70 | rspec (>= 2.5.0) 71 | simplecov 72 | sqlite3 73 | yard 74 | -------------------------------------------------------------------------------- /lib/acts_as_decimal/acts_as_decimal.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module ActsAsDecimal 3 | require 'bigdecimal' 4 | require 'action_view' 5 | include ActionView::Helpers::NumberHelper 6 | 7 | def self.included(base) 8 | base.extend(ClassMethods) 9 | end 10 | 11 | module ClassMethods 12 | # Implements integer handling as floating numbers. 13 | # Usage: 14 | # 15 | # Inside a model with a price or amount field, simply put 16 | # 17 | # acts_as_decimal field_name, :decimals => 2 18 | # 19 | def acts_as_decimal(attr_name, options = {:decimals => 2}) 20 | fields = [attr_name] unless attr_name.is_a? Array 21 | fields.each do |field| 22 | define_method "human_#{field}" do |*human_options| 23 | ActiveSupport::Deprecation.warn("acts_as_decimal: The human helper has been deprecated. Please use #{field}.number_with_precision, directly in your views. More info: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_precision") 24 | 25 | human_options = human_options.first || {:thousand_delimiters => true} 26 | return number_with_precision(self.send(field), :delimiter => (human_options[:thousand_delimiters] ? '.' : ''), :separator => ',', :precision => 2) 27 | end 28 | 29 | define_method "#{field}" do 30 | if value = read_attribute("#{field}") 31 | value / BigDecimal('10').power("#{options[:decimals]}".to_i).to_f 32 | end 33 | end 34 | 35 | define_method "#{field}=" do |decimalnum| 36 | value = unless decimalnum.nil? 37 | (BigDecimal.new(decimalnum.to_s) * BigDecimal('10').power("#{options[:decimals]}".to_i)).to_i 38 | end 39 | write_attribute("#{field}", value || nil) 40 | end 41 | 42 | define_method "#{field}_raw" do 43 | read_attribute("#{field}") 44 | end 45 | 46 | define_method "#{field}_raw=" do |value| 47 | write_attribute("#{field}", value) 48 | end 49 | end 50 | end 51 | 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /spec/acts_as_decimal_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Product do 4 | context "with a price of 10.30" do 5 | 6 | before(:all) do 7 | Product.acts_as_decimal :price 8 | end 9 | 10 | subject { Product.new(:price => 10.30)} 11 | 12 | it "has a price of 10.30" do 13 | subject.price.should == 10.30 14 | end 15 | 16 | it { should respond_to(:price_raw) } 17 | 18 | it "has a price_raw of 1030" do 19 | subject.price_raw.should == 1030 20 | end 21 | 22 | it "sets a price_raw of 2030" do 23 | subject.price_raw = 2030 24 | subject.price.should == 20.30 25 | end 26 | 27 | it "stores 1030 to the database" do 28 | subject.attributes["price"].should == 1030 29 | end 30 | 31 | it "handles nil values with dignity" do 32 | subject.price = nil 33 | 34 | subject.price.should be_nil 35 | subject.price_raw.should be_nil 36 | end 37 | 38 | end 39 | 40 | describe "human helpers", "when retrieving the human value (a pretty string)" do 41 | 42 | context "with a 3.900.400,00 price" do 43 | 44 | subject { Product.new(:price => 3_900_400.00) } 45 | 46 | it "has a human_price of 3.900.400,00" do 47 | subject.human_price.should == "3.900.400,00" 48 | end 49 | 50 | it "has a human_price(:thousand_delimiters => false) of 3900400,00" do 51 | subject.human_price(:thousand_delimiters => false).should == "3900400,00" 52 | end 53 | 54 | end 55 | 56 | context "with a 20.000,00 price" do 57 | 58 | subject { Product.new(:price => 20000.00) } 59 | 60 | it "has a human_price of 20.000,00" do 61 | subject.human_price.should == "20.000,00" 62 | end 63 | 64 | it "has a human_price(:thousand_delimiters => false) of 20000,00" do 65 | subject.human_price(:thousand_delimiters => false).should == "20000,00" 66 | end 67 | 68 | end 69 | 70 | context "with a 1234.56 price" do 71 | 72 | subject { Product.new(:price => 1234.56) } 73 | 74 | it "has a human_price of 1.234,56" do 75 | subject.human_price.should == "1.234,56" 76 | end 77 | 78 | it "has a human_price(:thousand_delimiters => false) of 1234,56" do 79 | subject.human_price(:thousand_delimiters => false).should == "1234,56" 80 | end 81 | 82 | end 83 | 84 | context "with a 900,00 price" do 85 | 86 | subject { Product.new(:price => 900.00) } 87 | 88 | it "has a human_price of 900,00" do 89 | subject.human_price.should == "900,00" 90 | end 91 | 92 | it "has a human_price(:thousand_delimiters => false) of 900,00" do 93 | subject.human_price(:thousand_delimiters => false).should == "900,00" 94 | end 95 | 96 | end 97 | 98 | context "with a string price of '900.00'" do 99 | 100 | subject { Product.new(:price => "900.00") } 101 | 102 | it "has a human_price of 900,00" do 103 | subject.human_price.should == "900,00" 104 | end 105 | 106 | it "has a human_price(:thousand_delimiters => false) of 900,00" do 107 | subject.human_price(:thousand_delimiters => false).should == "900,00" 108 | end 109 | 110 | end 111 | 112 | context "with a negative float price -900.00" do 113 | 114 | subject { Product.new(:price => -900.00) } 115 | 116 | it "has a human_price of -900,00" do 117 | subject.human_price.should == "-900,00" 118 | end 119 | 120 | it "has a human_price(:thousand_delimiters => false) of -900,00" do 121 | subject.human_price(:thousand_delimiters => false).should == "-900,00" 122 | end 123 | 124 | end 125 | 126 | context "with a negative string price '-900.00'" do 127 | 128 | subject { Product.new(:price => "-900.00") } 129 | 130 | it "has a human_price of -900,00" do 131 | subject.human_price.should == "-900,00" 132 | end 133 | 134 | it "has a human_price(:thousand_delimiters => false) of -900,00" do 135 | subject.human_price(:thousand_delimiters => false).should == "-900,00" 136 | end 137 | 138 | end 139 | end 140 | 141 | context "with a price of 10.30452 and specifying :decimals => 5" do 142 | 143 | before(:all) do 144 | Product.acts_as_decimal :price, :decimals => 5 145 | end 146 | 147 | subject { Product.new(:price => 10.30452) } 148 | 149 | it "has a price of 10.30452" do 150 | subject.price.should == 10.30452 151 | end 152 | 153 | it "has a price_raw of 10304542" do 154 | subject.price_raw.should == 1030452 155 | end 156 | 157 | it "stores 1030452 to the database" do 158 | subject.attributes["price"].should == 1030452 159 | end 160 | 161 | end 162 | 163 | end 164 | --------------------------------------------------------------------------------