diff --git a/lib/fbe/award.rb b/lib/fbe/award.rb index ae6eeb43..f1fc3d9c 100644 --- a/lib/fbe/award.rb +++ b/lib/fbe/award.rb @@ -379,6 +379,8 @@ def line(value, text) # Calculates the total points in this bill. # # @return [Integer] The sum of all point values, rounded to an integer + # @note The sum is rounded once, at the end, not line by line, so that + # a clause worth half a point costs half a point and not a whole one # @example # bill = Fbe::Award::Bill.new # bill.line(42.5, "for answer") @@ -396,7 +398,11 @@ def points # bill.line(25, "for documentation") # bill.greeting #=> "You've earned +75 points for this: +50 for code review; +25 for documentation. " def greeting - items = @lines.map { |l| "#{format('%+d', whole(l[:v]))} #{l[:t]}" } + items = + @lines.zip(portions).filter_map do |l, v| + next if v.zero? + "#{format('%+d', v)} #{l[:t]}" + end case items.size when 0 "You've earned nothing. " @@ -416,6 +422,24 @@ def greeting def whole(value) Integer(Float(value).round) end + + # Splits the total between the lines, giving each one a whole number. + # + # Every line is rounded on its own first, and then the difference + # between what that adds up to and the total, which is rounded once, + # is handed to the lines with the largest remainders. So the numbers + # in the greeting still add up to the number in front of it. + # + # @return [Array] One whole number per line, summing to +points+ + def portions + ints = @lines.map { |l| whole(l[:v]) } + diff = points - ints.sum + return ints if diff.zero? + step = diff.negative? ? -1 : 1 + order = (0...ints.size).sort_by { |i| (ints[i] - @lines[i][:v]) * step } + diff.abs.times { |k| ints[order[k % ints.size]] += step } + ints + end end # A class for generating human-readable bylaws. diff --git a/test/fbe/test_award.rb b/test/fbe/test_award.rb index b13c5c3d..41c732fb 100644 --- a/test/fbe/test_award.rb +++ b/test/fbe/test_award.rb @@ -174,6 +174,24 @@ def test_rounds_the_sum_once assert_equal(1, bill.points) end + def test_rounds_the_total_and_not_every_line + b = Fbe::Award.new('(award (give 8 "as a basis") (give -0.5 "for one day of delay"))').bill + assert_equal(8, b.points, b.greeting) + b = Fbe::Award.new('(award (give 8 "as a basis") (give -1.5 "for three days of delay"))').bill + assert_equal(7, b.points, b.greeting) + end + + def test_lines_add_up_to_the_total_with_halves + b = Fbe::Award.new('(award (give 8 "as a basis") (give -1.5 "for delay"))').bill + g = b.greeting + assert_equal("You've earned +7 points for this: +8 as a basis; -1 for delay. ", g) + end + + def test_skips_a_line_that_is_worth_nothing + g = Fbe::Award.new('(award (give 8 "as a basis") (give -0.4 "for delay"))').bill.greeting + assert_equal("You've earned +8 points. ", g) + end + def test_shorten_when_one_number g = Fbe::Award.new('(award (give 23 "for love"))').bill.greeting assert_equal('You\'ve earned +23 points. ', g, g)