├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── calc.gemspec ├── lib ├── calc.rb └── calc │ └── version.rb └── test └── calc.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | .idea 3 | *.gem 4 | .bundle 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in calc.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | calc (1.0.0) 5 | 6 | GEM 7 | remote: http://rubygems.org/ 8 | specs: 9 | 10 | PLATFORMS 11 | ruby 12 | 13 | DEPENDENCIES 14 | calc! 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Calc 2 | 3 | Calc is safe, simple, pure Ruby mathematical expressions evaluator (calculator) library. 4 | 5 | Although based on Ruby 'eval', it takes special care to sanitize the expression. 6 | 7 | Calc supports all basic mathematical operations +, -, \*, / and the power function (through ** operator). 8 | Calc also supports parenthesis and nesting. 9 | Calc does NOT support advanced mathematical functions like trigonometry, logarithm, etc. 10 | 11 | Calc plays well with Rails ActiveModel::Validations. 12 | If the supplied expression is invalid (nil, blank, or syntax error), 13 | it returns the expression itself verbatim, 14 | thus allowing to handle errors via Rails validations. 15 | 16 | ## Usage 17 | 18 | require 'calc' 19 | 20 | Calc.evaluate( "2 + 2" ) # => 4 21 | 22 | Calc.evaluate( "2 * (1 + 9)" ) # => 20 23 | 24 | Calc.evaluate( "2 ** (1.0 / 2)" ) # => 1.4142135623730951 (the square of two) 25 | 26 | Calc.evaluate( "( ( 2 - 3 ) / 13 * ( 50 + 325.843 ) ) ** 3" ) # => -53090815.704202116 27 | 28 | ## Installation 29 | 30 | gem install calc 31 | 32 | ## Using Bundler? 33 | 34 | Add the following to your Gemfile: 35 | 36 | gem 'calc' 37 | 38 | Then run as usual: 39 | 40 | bundle 41 | 42 | ## Calc is safe 43 | 44 | Calc defines a white list of characters allowed in the expression to prevent code injection attacks. 45 | All letters are outlawed as well as majority of other characters. Calc comes with tests. 46 | 47 | ## License 48 | 49 | Copyright (C) 2010 Piotr 'Qertoip' Włodarek. Distributed under the MIT License. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | -------------------------------------------------------------------------------- /calc.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "calc/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "calc" 7 | s.version = Calc::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Piotr 'Qertoip' Włodarek"] 10 | s.email = ["qertoip@gmail.com"] 11 | s.homepage = "" 12 | s.summary = %q{Calculator (mathematical expressions evaluator) library for Ruby} 13 | s.description = %q{Calc is safe, simple, pure-ruby mathematical expressions evaluator (calculator) library. Although based on Ruby 'eval', it takes special care to sanitize the expression.} 14 | 15 | s.rubyforge_project = "calc" 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 20 | s.require_paths = ["lib"] 21 | end 22 | -------------------------------------------------------------------------------- /lib/calc.rb: -------------------------------------------------------------------------------- 1 | module Calc 2 | 3 | def self.evaluate( unsafe_expression ) 4 | return nil if unsafe_expression.nil? 5 | 6 | safe_expression = sanitize( unsafe_expression ) 7 | 8 | return unsafe_expression if blank?( safe_expression ) 9 | 10 | begin 11 | return eval( safe_expression ) 12 | rescue SyntaxError, NoMethodError 13 | # return expression verbatim so ActiveModel::Validations could take care of this 14 | return unsafe_expression 15 | end 16 | end 17 | 18 | def self.evaluate_to_int( unsafe_expression ) 19 | return nil if unsafe_expression.nil? 20 | return unsafe_expression if unsafe_expression.blank? 21 | return evaluate( unsafe_expression ).to_i 22 | end 23 | 24 | private 25 | 26 | def self.sanitize( unsafe_expression ) 27 | allowed_characters = Regexp.escape( '+-*/.()' ) 28 | return unsafe_expression.gsub( /[^\d#{allowed_characters}]/, '' ) 29 | end 30 | 31 | def self.blank?( s ) 32 | s =~ /^\s*$/ 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /lib/calc/version.rb: -------------------------------------------------------------------------------- 1 | module Calc 2 | VERSION = "1.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /test/calc.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path( "../../lib/calc", __FILE__ ) 2 | require 'test/unit' 3 | 4 | class CalcTest < Test::Unit::TestCase 5 | 6 | # .evaluate() 7 | 8 | def test_evaluate_returns_correct_value_when_given_valid_expression 9 | assert_equal( 2, Calc.evaluate( "1+1" ) ) 10 | assert_equal( 6, Calc.evaluate( " 3 * 2 " ) ) 11 | assert_equal( 10, Calc.evaluate( "5 * ( 1 + 1 )" ) ) 12 | assert_equal( 25, Calc.evaluate( "5 ** ( 1 + 1 )" ) ) 13 | assert_equal( 0, Calc.evaluate( "5 ** ( 1 + 1 ) / 5 - 5" ) ) 14 | assert_in_delta( 0.5, Calc.evaluate( "1.0/2" ), 0.0000001 ) 15 | end 16 | 17 | def test_evaluate_returns_nil_when_given_nil 18 | assert_nil( Calc.evaluate( nil ) ) 19 | end 20 | 21 | def test_evaluate_returns_expression_verbatim_when_given_blank_expression 22 | blank_expression = " \r \n " 23 | assert_equal( blank_expression, Calc.evaluate( blank_expression ) ) 24 | assert_equal( "", Calc.evaluate( "" ) ) 25 | end 26 | 27 | def test_evaluate_returns_expression_verbatim_when_given_invalid_expression 28 | invalid_expression_1 = "2//3" 29 | assert_equal( invalid_expression_1, Calc.evaluate( invalid_expression_1 ) ) 30 | invalid_expression_2 = "2 * (5 + 4" 31 | assert_equal( invalid_expression_2, Calc.evaluate( invalid_expression_2 ) ) 32 | end 33 | 34 | def test_evaluate_is_code_injection_proof 35 | payload_1 = "raise 'evil'" 36 | assert_nothing_raised do 37 | Calc.evaluate( payload_1 ) 38 | end 39 | 40 | payload_2 = "binding" 41 | assert_equal( "binding", Calc.evaluate( payload_2 ) ) # returned verbatim - not evaled 42 | 43 | payload_3 = "__FILE__" 44 | assert_equal( "__FILE__", Calc.evaluate( payload_3 ) ) 45 | end 46 | 47 | end --------------------------------------------------------------------------------