# frozen_string_literal: true require 'spec_helper' RSpec.describe "Allocator" do describe "allocate"do specify "#allocate takes no action when one gets all" do expect(new_allocator(5).allocate([1])).to eq([Money.new(5)]) end specify "#allocate does not lose pennies" do moneys = new_allocator(0.05).allocate([0.3,0.7]) expect(moneys[0]).to eq(Money.new(0.02)) expect(moneys[1]).to eq(Money.new(0.03)) end specify "#allocate does not lose dollars with non-decimal currency" do moneys = new_allocator(5, 'JPY').allocate([0.3,0.7]) expect(moneys[0]).to eq(Money.new(2, 'JPY')) expect(moneys[1]).to eq(Money.new(3, 'JPY')) end specify "#allocate does not lose dollars with three decimal currency" do moneys = new_allocator(0.005, 'JOD').allocate([0.3,0.7]) expect(moneys[0]).to eq(Money.new(0.002, 'JOD')) expect(moneys[1]).to eq(Money.new(0.003, 'JOD')) end specify "#allocate does not lose pennies even when given a lossy split" do moneys = new_allocator(1).allocate([0.333,0.333, 0.333]) expect(moneys[0].subunits).to eq(34) expect(moneys[1].subunits).to eq(33) expect(moneys[2].subunits).to eq(33) end specify "#allocate requires total to be less than 1" do expect { new_allocator(0.05).allocate([0.5,0.6]) }.to raise_error(ArgumentError) end specify "#allocate will use rationals if provided" do splits = [128400,20439,14589,14589,25936].map{ |num| Rational(num, 203953) } # sums to > 1 if converted to float expect(new_allocator(2.25).allocate(splits)).to eq([Money.new(1.42), Money.new(0.23), Money.new(0.16), Money.new(0.16), Money.new(0.28)]) end specify "#allocate will convert rationals with high precision" do ratios = [Rational(1, 1), Rational(0)] expect(new_allocator("858993456.12").allocate(ratios)).to eq([Money.new("858993456.12"), Money.new(0, Money::NULL_CURRENCY)]) ratios = [Rational(1, 6), Rational(5, 6)] expect(new_allocator("3.00").allocate(ratios)).to eq([Money.new("0.50"), Money.new("2.50")]) end specify "#allocate doesn't raise with weird negative rational ratios" do rate = Rational(-5, 1201) expect { new_allocator(1).allocate([rate, 1 - rate]) }.not_to raise_error end specify "#allocate fills pennies from beginning to end with roundrobin strategy" do moneys = new_allocator(0.05).allocate([0.3,0.7], :roundrobin) expect(moneys[0]).to eq(Money.new(0.02)) expect(moneys[1]).to eq(Money.new(0.03)) end specify "#allocate fills pennies from end to beginning with roundrobin_reverse strategy" do moneys = new_allocator(0.05).allocate([0.3,0.7], :roundrobin_reverse) expect(moneys[0]).to eq(Money.new(0.01)) expect(moneys[1]).to eq(Money.new(0.04)) end specify "#allocate raise ArgumentError when invalid strategy is provided" do expect { new_allocator(0.03).allocate([0.5, 0.5], :bad_strategy_name) }.to raise_error(ArgumentError, "Invalid strategy. Valid options: :roundrobin, :roundrobin_reverse") end specify "#allocate does not raise ArgumentError when invalid splits types are provided" do moneys = new_allocator(0.03).allocate([0.5, 0.5], :roundrobin) expect(moneys[0]).to eq(Money.new(0.02)) expect(moneys[1]).to eq(Money.new(0.01)) end end describe 'allocate_max_amounts' do specify "#allocate_max_amounts returns the weighted allocation without exceeding the maxima when there is room for the remainder" do expect( new_allocator(30.75).allocate_max_amounts([Money.new(26), Money.new(4.75)]), ).to eq([Money.new(26), Money.new(4.75)]) end specify "#allocate_max_amounts returns the weighted allocation without exceeding the maxima when there is room for the remainder with currency" do expect( new_allocator(3075, 'JPY').allocate_max_amounts([Money.new(2600, 'JPY'), Money.new(475, 'JPY')]), ).to eq([Money.new(2600, 'JPY'), Money.new(475, 'JPY')]) end specify "#allocate_max_amounts legal computation with no currency objects" do expect( new_allocator(3075, 'JPY').allocate_max_amounts([2600, 475]), ).to eq([Money.new(2600, 'JPY'), Money.new(475, 'JPY')]) expect( new_allocator(3075, Money::NULL_CURRENCY).allocate_max_amounts([Money.new(2600, 'JPY'), Money.new(475, 'JPY')]), ).to eq([Money.new(2600, 'JPY'), Money.new(475, 'JPY')]) end specify "#allocate_max_amounts illegal computation across currencies" do expect { new_allocator(3075, 'USD').allocate_max_amounts([Money.new(2600, 'JPY'), Money.new(475, 'JPY')]) }.to raise_error(ArgumentError) end specify "#allocate_max_amounts drops the remainder when returning the weighted allocation without exceeding the maxima when there is no room for the remainder" do expect( new_allocator(30.75).allocate_max_amounts([Money.new(26), Money.new(4.74)]), ).to eq([Money.new(26), Money.new(4.74)]) end specify "#allocate_max_amounts returns the weighted allocation when there is no remainder" do expect( new_allocator(30).allocate_max_amounts([Money.new(15), Money.new(15)]), ).to eq([Money.new(15), Money.new(15)]) end specify "#allocate_max_amounts allocates the remainder round-robin when the maxima are not reached" do expect( new_allocator(1).allocate_max_amounts([Money.new(33), Money.new(33), Money.new(33)]), ).to eq([Money.new(0.34), Money.new(0.33), Money.new(0.33)]) end specify "#allocate_max_amounts allocates up to the maxima specified" do expect( new_allocator(100).allocate_max_amounts([Money.new(5), Money.new(2)]), ).to eq([Money.new(5), Money.new(2)]) end specify "#allocate_max_amounts supports all-zero maxima" do expect( new_allocator(3).allocate_max_amounts([Money.new(0, Money::NULL_CURRENCY), Money.new(0, Money::NULL_CURRENCY), Money.new(0, Money::NULL_CURRENCY)]), ).to eq([Money.new(0, Money::NULL_CURRENCY), Money.new(0, Money::NULL_CURRENCY), Money.new(0, Money::NULL_CURRENCY)]) end specify "#allocate_max_amounts allocates the right amount without rounding error" do expect( new_allocator(24.2).allocate_max_amounts([Money.new(46), Money.new(46), Money.new(50), Money.new(50),Money.new(50)]), ).to eq([Money.new(4.6), Money.new(4.6), Money.new(5), Money.new(5), Money.new(5)]) end end def new_allocator(amount, currency = nil) Money::Allocator.new(Money.new(amount, currency)) end end