Skip to content

Commit

Permalink
simplify function registration and update README
Browse files Browse the repository at this point in the history
Closes #52
  • Loading branch information
rubysolo committed Aug 6, 2015
1 parent a1fa1fa commit 77435bb
Show file tree
Hide file tree
Showing 12 changed files with 52 additions and 99 deletions.
87 changes: 28 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ Dentaku
=======

[![Join the chat at https://gitter.im/rubysolo/dentaku](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/rubysolo/dentaku?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

[![Gem Version](https://badge.fury.io/rb/dentaku.png)](http://badge.fury.io/rb/dentaku)
[![Build Status](https://travis-ci.org/rubysolo/dentaku.png?branch=master)](https://travis-ci.org/rubysolo/dentaku)
[![Code Climate](https://codeclimate.com/github/rubysolo/dentaku.png)](https://codeclimate.com/github/rubysolo/dentaku)
Expand Down Expand Up @@ -68,10 +67,9 @@ calculator.evaluate!('10 * x')
Dentaku::UnboundVariableError: Dentaku::UnboundVariableError
```

A number of functions are also supported. Okay, the number is currently five,
but more will be added soon. The current functions are
`if`, `not`, `round`, `rounddown`, and `roundup`, and they work like their
counterparts in Excel:
Dentaku has built-in functions (including `if`, `not`, `min`, `max`, and
`round`) and the ability to define custom functions (see below). Functions
generally work like their counterparts in Excel:

```ruby
calculator.evaluate('if (pears < 10, 10, 20)', pears: 5)
Expand All @@ -80,7 +78,7 @@ calculator.evaluate('if (pears < 10, 10, 20)', pears: 15)
#=> 20
```

`round`, `rounddown`, and `roundup` can be called with or without the number of decimal places:
`round` can be called with or without the number of decimal places:

```ruby
calculator.evaluate('round(8.2)')
Expand All @@ -89,7 +87,8 @@ calculator.evaluate('round(8.2759, 2)')
#=> 8.28
```

`round` and `rounddown` round down, while `roundup` rounds up.
`round` follows rounding rules, while `roundup` and `rounddown` are `ceil` and
`floor`, respectively.

If you're too lazy to be building calculator objects, there's a shortcut just
for you:
Expand All @@ -107,7 +106,7 @@ Math: `+ - * / %`

Logic: `< > <= >= <> != = AND OR`

Functions: `IF NOT ROUND ROUNDDOWN ROUNDUP`
Functions: `IF NOT MIN MAX ROUND ROUNDDOWN ROUNDUP`

Math: all functions from Ruby's Math module, including `SIN, COS, TAN, ...`

Expand All @@ -129,7 +128,7 @@ need_to_compute = {
In the example, `annual_income` needs to be computed (and stored) before
`income_taxes`.

Dentaku provides two methods to help resolve formulas in order`:
Dentaku provides two methods to help resolve formulas in order:

#### Calculator.dependencies
Pass a (string) expression to Dependencies and get back a list of variables (as
Expand All @@ -148,7 +147,7 @@ calc.dependencies("annual_income / 5")
#### Calculator.solve!
Have Dentaku figure out the order in which your formulas need to be evaluated.

Pass in a hash of {eventual_variable_name: "expression"} to `solve!` and
Pass in a hash of `{eventual_variable_name: "expression"}` to `solve!` and
have Dentaku figure out dependencies (using `TSort`) for you.

Raises `TSort::Cyclic` when a valid expression order cannot be found.
Expand All @@ -157,7 +156,7 @@ Raises `TSort::Cyclic` when a valid expression order cannot be found.
calc = Dentaku::Calculator.new
calc.store(monthly_income: 50)
need_to_compute = {
income_taxes: "annual_income / 5",
income_taxes: "annual_income / 5",
annual_income: "monthly_income * 12"
}
calc.solve!(need_to_compute)
Expand All @@ -173,7 +172,8 @@ calc.solve!(
INLINE COMMENTS
---------------------------------

If your expressions grow long or complex, you may add inline comments for future reference. This is particularly useful if you save your expressions in a model.
If your expressions grow long or complex, you may add inline comments for future
reference. This is particularly useful if you save your expressions in a model.

```ruby
calculator.evaluate('kiwi + 5 /* This is a comment */', kiwi: 2)
Expand All @@ -200,69 +200,39 @@ need. Please implement your favorites and send a pull request! Okay, so maybe
that's not feasible because:

1. You can't be bothered to share
2. You can't wait for me to respond to a pull request, you need it `NOW()`
3. The formula is the secret sauce for your startup
1. You can't wait for me to respond to a pull request, you need it `NOW()`
1. The formula is the secret sauce for your startup

Whatever your reasons, Dentaku supports adding functions at runtime. To add a
function, you'll need to specify:

* Name
* Return type
* Signature
* Body

Naming can be the hardest part, so you're on your own for that.

`:type` specifies the type of value that will be returned, most likely
`:numeric`, `:string`, or `:logical`.

`:signature` specifies the types and order of the parameters for your function.
function, you'll need to specify a name and a lambda that accepts all function
arguments and returns the result value.

`:body` is a lambda that implements your function. It is passed the arguments
and should return the calculated value.

As an example, the exponentiation function takes two parameters, the mantissa
and the exponent, so the token list could be defined as: `[:numeric,
:numeric]`. Other functions might be variadic -- consider `max`, a function
that takes any number of numeric inputs and returns the largest one. Its token
list could be defined as: `[:arguments]` (one or more numeric, string, or logical
values, separated by commas). See the
[rules definitions](https://github.com/rubysolo/dentaku/blob/master/lib/dentaku/token_matcher.rb#L87)
for the names of token patterns you can use.

Functions can be added individually using Calculator#add_function, or en masse using
Calculator#add_functions.

Here's an example of adding the `exp` function:
Here's an example of adding a function named `POW` that implements
exponentiation.

```ruby
> c = Dentaku::Calculator.new
> c.add_function(
name: :exp,
type: :numeric,
signature: [:numeric, :numeric],
body: ->(mantissa, exponent) { mantissa ** exponent }
)
> c.evaluate('EXP(3,2)')
> c.add_function(:pow, ->(mantissa, exponent) { mantissa ** exponent })
> c.evaluate('POW(3,2)')
#=> 9
> c.evaluate('EXP(2,3)')
> c.evaluate('POW(2,3)')
#=> 8
```

Here's an example of adding the `max` function:
Here's an example of adding a variadic function:

```ruby
> c = Dentaku::Calculator.new
> c.add_function(
name: :max,
type: :numeric,
signature: [:arguments],
body: ->(*args) { args.max }
)
> c.add_function(:max, ->(*args) { args.max })
> c.evaluate 'MAX(8,6,7,5,3,0,9)'
#=> 9
```

(However both of these are already built-in -- the `^` operator and the `MAX`
function)

Functions can be added individually using Calculator#add_function, or en masse
using Calculator#add_functions.

THANKS
------
Expand Down Expand Up @@ -309,4 +279,3 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

2 changes: 1 addition & 1 deletion lib/dentaku/ast/function.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def self.get(name)
registry.fetch(function_name(name)) { fail "Undefined function #{ name } "}
end

def self.register(name, return_type, arguments, implementation)
def self.register(name, implementation)
function = Class.new(self) do
def self.implementation=(impl)
@implementation = impl
Expand Down
2 changes: 1 addition & 1 deletion lib/dentaku/ast/functions/max.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require_relative '../function'

Dentaku::AST::Function.register(:max, :numeric, [:arguments], ->(*args) {
Dentaku::AST::Function.register(:max, ->(*args) {
args.max
})
2 changes: 1 addition & 1 deletion lib/dentaku/ast/functions/min.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require_relative '../function'

Dentaku::AST::Function.register(:min, :numeric, [:arguments], ->(*args) {
Dentaku::AST::Function.register(:min, ->(*args) {
args.min
})
2 changes: 1 addition & 1 deletion lib/dentaku/ast/functions/not.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require_relative '../function'

Dentaku::AST::Function.register(:not, :logical, [:logical], ->(logical) {
Dentaku::AST::Function.register(:not, ->(logical) {
! logical
})
2 changes: 1 addition & 1 deletion lib/dentaku/ast/functions/round.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require_relative '../function'

Dentaku::AST::Function.register(:round, :numeric, [:arguments], ->(numeric, places=nil) {
Dentaku::AST::Function.register(:round, ->(numeric, places=nil) {
numeric.round(places || 0)
})
2 changes: 1 addition & 1 deletion lib/dentaku/ast/functions/rounddown.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require_relative '../function'

Dentaku::AST::Function.register(:rounddown, :numeric, [:numeric], ->(numeric) {
Dentaku::AST::Function.register(:rounddown, ->(numeric) {
numeric.floor
})
2 changes: 1 addition & 1 deletion lib/dentaku/ast/functions/roundup.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require_relative '../function'

Dentaku::AST::Function.register(:roundup, :numeric, [:numeric], ->(numeric) {
Dentaku::AST::Function.register(:roundup, ->(numeric) {
numeric.ceil
})
2 changes: 1 addition & 1 deletion lib/dentaku/ast/functions/ruby_math.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
require_relative "../function"

Math.methods(false).each do |method|
Dentaku::AST::Function.register(method, :numeric, [:arguments], ->(*args) {
Dentaku::AST::Function.register(method, ->(*args) {
Math.send(method, *args)
})
end
6 changes: 3 additions & 3 deletions lib/dentaku/calculator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ def initialize
@tokenizer = Tokenizer.new
end

def add_function(fn)
Dentaku::AST::Function.register(fn[:name], fn[:type], fn[:signature], fn[:body])
def add_function(name, body)
Dentaku::AST::Function.register(name, body)
self
end

def add_functions(fns)
fns.each { |fn| add_function(fn) }
fns.each { |(name, body)| add_function(name, body) }
self
end

Expand Down
2 changes: 1 addition & 1 deletion spec/ast/function_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
end

it 'registers a custom function' do
described_class.register("flarble", :string, [], -> { "flarble" })
described_class.register("flarble", -> { "flarble" })
expect { described_class.get("flarble") }.not_to raise_error
function = described_class.get("flarble").new
expect(function.value).to eq "flarble"
Expand Down
40 changes: 12 additions & 28 deletions spec/external_function_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,12 @@
let(:with_external_funcs) do
c = described_class.new

now = { name: :now, type: :string, signature: [], body: -> { Time.now.to_s } }
c.add_function(now)
c.add_function(:now, -> { Time.now.to_s })

fns = [
{
name: :cexp,
type: :numeric,
signature: [ :numeric, :numeric ],
body: ->(mantissa, exponent) { mantissa ** exponent }
},
{
name: :max,
type: :numeric,
signature: [ :arguments ],
body: ->(*args) { args.max }
},
{
name: :min,
type: :numeric,
signature: [ :arguments ],
body: ->(*args) { args.min }
}
[:pow, ->(mantissa, exponent) { mantissa ** exponent }],
[:biggest, ->(*args) { args.max }],
[:smallest, ->(*args) { args.min }],
]

c.add_functions(fns)
Expand All @@ -41,18 +25,18 @@
expect(now).not_to be_empty
end

it 'includes CEXP' do
expect(with_external_funcs.evaluate('CEXP(2,3)')).to eq(8)
expect(with_external_funcs.evaluate('CEXP(3,2)')).to eq(9)
expect(with_external_funcs.evaluate('CEXP(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
it 'includes POW' do
expect(with_external_funcs.evaluate('POW(2,3)')).to eq(8)
expect(with_external_funcs.evaluate('POW(3,2)')).to eq(9)
expect(with_external_funcs.evaluate('POW(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
end

it 'includes MAX' do
expect(with_external_funcs.evaluate('MAX(8,6,7,5,3,0,9)')).to eq(9)
it 'includes BIGGEST' do
expect(with_external_funcs.evaluate('BIGGEST(8,6,7,5,3,0,9)')).to eq(9)
end

it 'includes MIN' do
expect(with_external_funcs.evaluate('MIN(8,6,7,5,3,0,9)')).to eq(0)
it 'includes SMALLEST' do
expect(with_external_funcs.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
end
end
end
Expand Down

0 comments on commit 77435bb

Please sign in to comment.