Skip to content

Commit

Permalink
add INTERCEPT function
Browse files Browse the repository at this point in the history
Closes #296
  • Loading branch information
Tienduyvo authored and rubysolo committed Mar 12, 2024
1 parent c4964f6 commit 369f1da
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 2 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ Comparison: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`,

Logic: `IF`, `AND`, `OR`, `XOR`, `NOT`, `SWITCH`

Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`
Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`, `INTERCEPT`

Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L593))

Expand Down
3 changes: 2 additions & 1 deletion lib/dentaku/ast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
require_relative './ast/functions/duration'
require_relative './ast/functions/filter'
require_relative './ast/functions/if'
require_relative './ast/functions/intercept'
require_relative './ast/functions/map'
require_relative './ast/functions/max'
require_relative './ast/functions/min'
Expand All @@ -38,4 +39,4 @@
require_relative './ast/functions/string_functions'
require_relative './ast/functions/sum'
require_relative './ast/functions/switch'
require_relative './ast/functions/xor'
require_relative './ast/functions/xor'
35 changes: 35 additions & 0 deletions lib/dentaku/ast/functions/intercept.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require_relative '../function'

Dentaku::AST::Function.register(:intercept, :list, ->(*args) {
flatten_args = args.flatten
if flatten_args.length != 2 || !flatten_args.all? { |arg| arg.is_a?(Array) } || flatten_args.any? { |arg| arg.empty? }
raise Dentaku::ArgumentError.for(
:wrong_number_of_arguments,
function_name: 'INTERCEPT()', exact: 2, given: flatten_args.length
), 'INTERCEPT() requires exactly two arrays of numbers'
end

x_values, y_values = flatten_args
if x_values.length != y_values.length
raise Dentaku::ArgumentError.for(
:unequal_array_lengths,
function_name: 'INTERCEPT()'
), 'INTERCEPT() requires arrays of equal length'
end

x_values = x_values.map { |arg| Dentaku::AST::Function.numeric(arg) }
y_values = y_values.map { |arg| Dentaku::AST::Function.numeric(arg) }

x_sum = x_values.reduce(0, :+)
y_sum = y_values.reduce(0, :+)
xy_sum = x_values.zip(y_values).map { |x, y| x * y }.reduce(0, :+)
x_square_sum = x_values.map { |x| x**2 }.reduce(0, :+)

n = x_values.length

slope = (n * xy_sum - x_sum * y_sum) / (n * x_square_sum - x_sum**2)
intercept = y_values.reduce(:+) / n - slope * x_values.reduce(:+) / n

BigDecimal(intercept)
})

30 changes: 30 additions & 0 deletions spec/ast/intercept_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require 'spec_helper'
require 'dentaku/ast/functions/intercept'
require 'dentaku'

describe 'Dentaku::AST::Function::Intercept' do
it 'returns the correct intercept for given x and y arrays' do
x_values = [1, 2, 3, 4, 5]
y_values = [2, 3, 5, 4, 6]
result = Dentaku('INTERCEPT(?, ?)', x_values, y_values)
expect(result).to be_within(0.001).of(1.2)
end

context 'checking errors' do
it 'raises an error if arguments are not arrays' do
expect { Dentaku!("INTERCEPT(1, 2)") }.to raise_error(Dentaku::ArgumentError)
end

it 'raises an error if the arrays are not of equal length' do
x_values = [1, 2, 3]
y_values = [2, 3, 5, 4]
expect { Dentaku!("INTERCEPT(?, ?)", x_values, y_values) }.to raise_error(Dentaku::ArgumentError)
end

it 'raises an error if any of the arrays is empty' do
x_values = []
y_values = [2, 3, 5, 4]
expect { Dentaku!("INTERCEPT(?, ?)", x_values, y_values) }.to raise_error(Dentaku::ArgumentError)
end
end
end
7 changes: 7 additions & 0 deletions spec/calculator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,13 @@
expect(calculator.evaluate('NOT(some_boolean) AND -1 > 3', some_boolean: true)).to be_falsey
end

it 'calculates intercept correctly' do
x_values = [1, 2, 3, 4, 5]
y_values = [2, 3, 5, 4, 6]
result = calculator.evaluate('INTERCEPT(x_values, y_values)', x_values: x_values, y_values: y_values)
expect(result).to be_within(0.001).of(1.2)
end

describe "any" do
it "enumerates values and returns true if any evaluation is truthy" do
expect(calculator.evaluate!('any(xs, x, x > 3)', xs: [1, 2, 3, 4])).to be_truthy
Expand Down

0 comments on commit 369f1da

Please sign in to comment.