class Money::Allocator

Public Class Methods

new(money) click to toggle source
Calls superclass method
# File lib/money/allocator.rb, line 6
def initialize(money)
  super
end

Public Instance Methods

allocate(splits, strategy = :roundrobin) click to toggle source

@example left over pennies distributed reverse order when using roundrobin_reverse strategy

Money.new(10.01, "USD").allocate([0.5, 0.5], :roundrobin_reverse)
  #=> [#<Money value:5.00 currency:USD>, #<Money value:5.01 currency:USD>]
# File lib/money/allocator.rb, line 42
def allocate(splits, strategy = :roundrobin)
  splits.map!(&:to_r)
  allocations = splits.inject(0, :+)

  if (allocations - BigDecimal("1")) > Float::EPSILON
    raise ArgumentError, "splits add to more than 100%"
  end

  amounts, left_over = amounts_from_splits(allocations, splits)

  left_over.to_i.times do |i|
    amounts[allocation_index_for(strategy, amounts.length, i)] += 1
  end

  amounts.collect { |subunits| Money.from_subunits(subunits, currency) }
end
allocate_max_amounts(maximums) click to toggle source

Allocates money between different parties up to the maximum amounts specified. Left over pennies will be assigned round-robin up to the maximum specified. Pennies are dropped when the maximums are attained.

@example

Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.75)])
  #=> [Money.new(26), Money.new(4.75)]

Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.74)]
  #=> [Money.new(26), Money.new(4.74)]

Money.new(30).allocate_max_amounts([Money.new(15), Money.new(15)]
  #=> [Money.new(15), Money.new(15)]

Money.new(1).allocate_max_amounts([Money.new(33), Money.new(33), Money.new(33)])
  #=> [Money.new(0.34), Money.new(0.33), Money.new(0.33)]

Money.new(100).allocate_max_amounts([Money.new(5), Money.new(2)])
  #=> [Money.new(5), Money.new(2)]
# File lib/money/allocator.rb, line 78
def allocate_max_amounts(maximums)
  allocation_currency = extract_currency(maximums + [self.__getobj__])
  maximums = maximums.map { |max| max.to_money(allocation_currency) }
  maximums_total = maximums.reduce(Money.new(0, allocation_currency), :+)

  splits = maximums.map do |max_amount|
    next(Rational(0)) if maximums_total.zero?
    Money.rational(max_amount, maximums_total)
  end

  total_allocatable = [maximums_total.subunits, self.subunits].min

  subunits_amounts, left_over = amounts_from_splits(1, splits, total_allocatable)

  subunits_amounts.each_with_index do |amount, index|
    break unless left_over > 0

    max_amount = maximums[index].value * allocation_currency.subunit_to_unit
    next unless amount < max_amount

    left_over -= 1
    subunits_amounts[index] += 1
  end

  subunits_amounts.map { |cents| Money.from_subunits(cents, allocation_currency) }
end

Private Instance Methods

all_rational?(splits) click to toggle source
# File lib/money/allocator.rb, line 129
def all_rational?(splits)
  splits.all? { |split| split.is_a?(Rational) }
end
allocation_index_for(strategy, length, idx) click to toggle source
# File lib/money/allocator.rb, line 133
def allocation_index_for(strategy, length, idx)
  case strategy
  when :roundrobin
    idx % length
  when :roundrobin_reverse
    length - (idx % length) - 1
  else
    raise ArgumentError, "Invalid strategy. Valid options: :roundrobin, :roundrobin_reverse"
  end
end
amounts_from_splits(allocations, splits, subunits_to_split = subunits) click to toggle source
# File lib/money/allocator.rb, line 115
def amounts_from_splits(allocations, splits, subunits_to_split = subunits)
  raise ArgumentError, "All splits values must be of type Rational." unless all_rational?(splits)

  left_over = subunits_to_split

  amounts = splits.collect do |ratio|
    frac = (subunits_to_split * ratio / allocations.to_r).floor
    left_over -= frac
    frac
  end

  [amounts, left_over]
end
extract_currency(money_array) click to toggle source
# File lib/money/allocator.rb, line 107
def extract_currency(money_array)
  currencies = money_array.lazy.select { |money| money.is_a?(Money) }.reject(&:no_currency?).map(&:currency).to_a.uniq
  if currencies.size > 1
    raise ArgumentError, "operation not permitted for Money objects with different currencies #{currencies.join(', ')}"
  end
  currencies.first || NULL_CURRENCY
end