Price fraction
You are encouraged to solve this task according to the task description, using any language you may know.
A friend of mine runs a Pharmacy. He has a specialised rounding function in his Dispensary application which receives a decimal value of currency and forces it to a standard value. This value is regulated by a government department.
Task: Given a floating point value between 0.00 and 1.00, rescale according to the following table:
>= 0.00 < 0.06 := 0.10 >= 0.06 < 0.11 := 0.18 >= 0.11 < 0.16 := 0.26 >= 0.16 < 0.21 := 0.32 >= 0.21 < 0.26 := 0.38 >= 0.26 < 0.31 := 0.44 >= 0.31 < 0.36 := 0.50 >= 0.36 < 0.41 := 0.54 >= 0.41 < 0.46 := 0.58 >= 0.46 < 0.51 := 0.62 >= 0.51 < 0.56 := 0.66 >= 0.56 < 0.61 := 0.70 >= 0.61 < 0.66 := 0.74 >= 0.66 < 0.71 := 0.78 >= 0.71 < 0.76 := 0.82 >= 0.76 < 0.81 := 0.86 >= 0.81 < 0.86 := 0.90 >= 0.86 < 0.91 := 0.94 >= 0.91 < 0.96 := 0.98 >= 0.96 < 1.01 := 1.00
BASIC
This could also be done by building an array, but I felt that this was simpler.
<lang qbasic>DECLARE FUNCTION PriceFraction! (price AS SINGLE)
RANDOMIZE TIMER DIM x AS SINGLE x = RND PRINT x, PriceFraction(x)
FUNCTION PriceFraction! (price AS SINGLE)
'returns price unchanged if invalid value SELECT CASE price CASE IS < 0! PriceFraction! = price CASE IS < .06 PriceFraction! = .1 CASE IS < .11 PriceFraction! = .18 CASE IS < .16 PriceFraction! = .26 CASE IS < .21 PriceFraction! = .32 CASE IS < .26 PriceFraction! = .38 CASE IS < .31 PriceFraction! = .44 CASE IS < .36 PriceFraction! = .5 CASE IS < .41 PriceFraction! = .54 CASE IS < .46 PriceFraction! = .58 CASE IS < .51 PriceFraction! = .62 CASE IS < .56 PriceFraction! = .66 CASE IS < .61 PriceFraction! = .7 CASE IS < .66 PriceFraction! = .74 CASE IS < .71 PriceFraction! = .78 CASE IS < .76 PriceFraction! = .82 CASE IS < .81 PriceFraction! = .86 CASE IS < .86 PriceFraction! = .9 CASE IS < .91 PriceFraction! = .94 CASE IS < .96 PriceFraction! = .98 CASE IS < 1.01 PriceFraction! = 1! CASE ELSE PriceFraction! = price END SELECT
END FUNCTION</lang>
Sample outputs (run 5 times):
.7388727 .82 .8593103 .9 .826687 .9 .3444635 .5 .0491907 .1
C++
<lang C++>#include <iostream>
- include <cmath>
int main( ) {
double froms[ ] = { 0.00 , 0.06 , 0.11 , 0.16 , 0.21 , 0.26 , 0.31 , 0.36 , 0.41 , 0.46 , 0.51 , 0.56 , 0.61 , 0.66 , 0.71 , 0.76 , 0.81 , 0.86 , 0.91 , 0.96 } ; double tos[ ] = { 0.06 , 0.11 , 0.16 , 0.21 , 0.26 , 0.31 , 0.36 , 0.41 , 0.46 , 0.51 , 0.56 , 0.61 , 0.66 , 0.71 , 0.76 , 0.81 , 0.86 , 0.91 , 0.96 , 1.01 } ; double replacements [] = { 0.10 , 0.18 , 0.26 , 0.32 , 0.38 , 0.44 , 0.50 , 0.54 , 0.58 , 0.62 , 0.66 , 0.70 , 0.74 , 0.78 , 0.82 , 0.86 , 0.90 , 0.94 , 0.98 , 1.00 } ; double number = 0.1 ; std::cout << "Enter a fractional number between 0 and 1 ( 0 to end )!\n" ; std::cin >> number ; while ( number != 0 ) { if ( ( number < 0 ) || ( number > 1 ) ) {
std::cout << "Error! Only positive values between 0 and 1 are allowed!\n" ; return 1 ;
} double integerpart = floor ( number ) ; double remainder = number - integerpart ; int n = 0 ; while ( ! ( ( remainder >= froms[ n ] ) && ( remainder < tos[ n ] ) ) )
n++ ;
std::cout << "-->" << ( integerpart + replacements[ n ] ) << '\n' ; std::cout << "Enter a fractional number ( 0 to end )!\n" ; std::cin >> number ; } return 0 ;
} </lang>
Sample output: Enter a fractional number between 0 and 1 ( 0 to end )! 0.7 -->0.78 Enter a fractional number ( 0 to end )! 0.32 -->0.5 Enter a fractional number ( 0 to end )! 0.12 -->0.26 Enter a fractional number ( 0 to end )! 0
Clipper
<lang dbase>FUNCTION PriceFraction( npQuantDispensed )
LOCAL aPriceFraction := { {0,.06,.1},; {.06,.11,.18}, ; {.11,.16,.26}, ; {.16,.21,.32}, ; {.21,.26,.38}, ; {.26,.31,.44}, ; {.31,.36,.5}, ; {.36,.41,.54}, ; {.41,.46,.58}, ; {.46,.51,.62}, ; {.51,.56,.66}, ; {.56,.61,.7}, ; {.61,.66,.74}, ; {.66,.71,.78}, ; {.71,.76,.82}, ; {.76,.81,.86}, ; {.81,.86,.9}, ; {.86,.91,.94}, ; {.91,.96,.98} } LOCAL nResult LOCAL nScan IF npQuantDispensed = 0 nResult = 0 ELSEIF npQuantDispensed >= .96 nResult = 1 ELSE nScan := ASCAN( aPriceFraction, ; { |aItem| npQuantDispensed >= aItem[ 1 ] .AND.; npQuantDispensed < aItem[ 2 ] } ) nResult := aPriceFraction[ nScan ][ 3 ] END IF RETURN nResult</lang>
Fortran
<lang fortran>program price_fraction
implicit none integer, parameter :: i_max = 10 integer :: i real, dimension (20), parameter :: in = & & (/0.00, 0.06, 0.11, 0.16, 0.21, 0.26, 0.31, 0.36, 0.41, 0.46, & & 0.51, 0.56, 0.61, 0.66, 0.71, 0.76, 0.81, 0.86, 0.91, 0.96/) real, dimension (20), parameter :: out = & & (/0.10, 0.18, 0.26, 0.32, 0.38, 0.44, 0.50, 0.54, 0.58, 0.62, & & 0.66, 0.70, 0.74, 0.78, 0.82, 0.86, 0.90, 0.94, 0.98, 1.00/) real :: r
do i = 1, i_max call random_number (r) write (*, '(f8.6, 1x, f4.2)') r, out (maxloc (in, r >= in)) end do
end program price_fraction</lang> Sample output: <lang>0.997560 1.00 0.566825 0.70 0.965915 1.00 0.747928 0.82 0.367391 0.54 0.480637 0.62 0.073754 0.18 0.005355 0.10 0.347081 0.50 0.342244 0.50</lang>
J
Solution: <lang j>le =: -0.96 0.91 0.86 0.81 0.76 0.71 0.66 0.61 0.56 0.51 0.46 0.41 0.36 0.31 0.26 0.21 0.16 0.11 0.06 0.0 out =: 1.00 0.98 0.94 0.90 0.86 0.82 0.78 0.74 0.70 0.66 0.62 0.58 0.54 0.50 0.44 0.38 0.32 0.26 0.18 0.1
priceFraction =: out {~ le I. -</lang>
Example: <lang j> priceFraction 0.34 0.070145 0.06 0.05 0.50214 0.56 1 0.99 0 0.5 0.18 0.18 0.1 0.62 0.7 1 1 0.1</lang>
Oz
Using a for-loop with return and a default value for values >= 1.01. For out-of-range input, a "failed value" is returned, i.e. a value that throws an exception when it is accessed.
<lang oz>fun {PriceFraction X}
OutOfRange = {Value.failed outOfRange(X)}
in
for Limit#Result in [0.00#OutOfRange 0.06#0.10 0.11#0.18 0.16#0.26 0.21#0.32 0.26#0.38 0.31#0.44 0.36#0.5 0.41#0.54 0.46#0.58 0.51#0.62 0.56#0.66 0.61#0.70 0.66#0.74 0.71#0.78 0.76#0.82 0.81#0.86 0.86#0.90 0.91#0.94 0.96#0.98 1.01#1.00 ] return:Return default:OutOfRange do if X < Limit then {Return Result} end end
end</lang>
PureBasic
<lang PureBasic>Procedure.f PriceFraction(price.f)
;returns price unchanged if value is invalid Protected fraction Select price * 100 Case 0 To 5 fraction = 10 Case 06 To 10 fraction = 18 Case 11 To 15 fraction = 26 Case 16 To 20 fraction = 32 Case 21 To 25 fraction = 38 Case 26 To 30 fraction = 44 Case 31 To 35 fraction = 5 Case 36 To 40 fraction = 54 Case 41 To 45 fraction = 58 Case 46 To 50 fraction = 62 Case 51 To 55 fraction = 66 Case 56 To 60 fraction = 7 Case 61 To 65 fraction = 74 Case 66 To 70 fraction = 78 Case 71 To 75 fraction = 82 Case 76 To 80 fraction = 86 Case 81 To 85 fraction = 9 Case 86 To 90 fraction = 94 Case 91 To 95 fraction = 98 Case 96 To 100 fraction = 100 Default ProcedureReturn price EndSelect ProcedureReturn fraction / 100
EndProcedure
If OpenConsole()
Define x.f, i For i = 1 To 10 x = Random(10000)/10000 PrintN(StrF(x, 4) + " -> " + StrF(PriceFraction(x), 2)) Next Print(#CRLF$ + #CRLF$ + "Press ENTER to exit") Input() CloseConsole()
EndIf</lang> Sample output:
0.3793 -> 0.54 0.4425 -> 0.58 0.0746 -> 0.18 0.6918 -> 0.78 0.2993 -> 0.44 0.5486 -> 0.66 0.7848 -> 0.86 0.9383 -> 0.98 0.2292 -> 0.38 0.9560 -> 1.00
Python
Using the bisect standard module to reduce the comparisons with members of the cin array.
<lang python>>>> import bisect >>> def pricerounder(pricein): cin = [0.06, 0.11, 0.16, 0.21, 0.26, 0.31, 0.36, 0.41, 0.46, 0.51, 0.56, 0.61, 0.66, 0.71, 0.76, 0.81, 0.86, 0.91, 0.96, 1.01] cout = [0.10, 0.18, 0.26, 0.32, 0.38, 0.44, 0.50, 0.54, 0.58, 0.62, 0.66, 0.70, 0.74, 0.78, 0.82, 0.86, 0.90, 0.94, 0.98, 1.00] return cout[ bisect.bisect_right(cin, pricein) ]</lang>
When dealing with money it is good to think about possible loss of precision. If we change the units to be integer cents we could use the following exact routine: <lang python>>>> import bisect >>> _cin = [ 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56, 61, 66, 71, 76, 81, 86, 91, 96, 101] >>> _cout = [10, 18, 26, 32, 38, 44, 50, 54, 57, 62, 66, 70, 74, 78, 82, 86, 90, 94, 98, 100] >>> def centsrounder(centsin): return _cout[ bisect.bisect_right(_cin, centsin) ]</lang> Other options are to use the fractions or decimals modules for calculating money to a known precision.
Ruby
A simple function with hardcoded values. <lang ruby>def rescale_price_fraction(value)
raise ArgumentError, "value=#{value}, must have: 0 <= value < 1.01" if value < 0 || value >= 1.01 if value < 0.06 then 0.10 elsif value < 0.11 then 0.18 elsif value < 0.16 then 0.26 elsif value < 0.21 then 0.32 elsif value < 0.26 then 0.38 elsif value < 0.31 then 0.44 elsif value < 0.36 then 0.50 elsif value < 0.41 then 0.54 elsif value < 0.46 then 0.58 elsif value < 0.51 then 0.62 elsif value < 0.56 then 0.66 elsif value < 0.61 then 0.70 elsif value < 0.66 then 0.74 elsif value < 0.71 then 0.78 elsif value < 0.76 then 0.82 elsif value < 0.81 then 0.86 elsif value < 0.86 then 0.90 elsif value < 0.91 then 0.94 elsif value < 0.96 then 0.98 elsif value < 1.01 then 1.00 end
end</lang>
Or, where we can cut and paste the textual table in one place
for the String#lines
method.
For Ruby 1.8.6, use String#each_line
<lang ruby>class Price
ConversionTable = <<-END_OF_TABLE >= 0.00 < 0.06 := 0.10 >= 0.06 < 0.11 := 0.18 >= 0.11 < 0.16 := 0.26 >= 0.16 < 0.21 := 0.32 >= 0.21 < 0.26 := 0.38 >= 0.26 < 0.31 := 0.44 >= 0.31 < 0.36 := 0.50 >= 0.36 < 0.41 := 0.54 >= 0.41 < 0.46 := 0.58 >= 0.46 < 0.51 := 0.62 >= 0.51 < 0.56 := 0.66 >= 0.56 < 0.61 := 0.70 >= 0.61 < 0.66 := 0.74 >= 0.66 < 0.71 := 0.78 >= 0.71 < 0.76 := 0.82 >= 0.76 < 0.81 := 0.86 >= 0.81 < 0.86 := 0.90 >= 0.86 < 0.91 := 0.94 >= 0.91 < 0.96 := 0.98 >= 0.96 < 1.01 := 1.00 END_OF_TABLE
@@conversion_table = ConversionTable.lines.collect do |line| line.scan(/\d\.\d\d/).collect {|str| str.to_f} end
MIN = @@conversion_table[0][0] MAX = @@conversion_table[-1][1]
def initialize(value) if value < MIN || value >= MAX raise ArgumentError, "value=#{value}, must have: #{MIN} <= value < #{MAX}" end @standard_value = (@@conversion_table.find do |lower, upper, standard| value.between?(lower, upper) end)[2] end attr_reader :standard_value
end</lang>
And a test suite <lang ruby>require 'test/unit'
class PriceFractionTests < Test::Unit::TestCase
@@ok_tests = [ [0.3793, 0.54], [0.4425, 0.58], [0.0746, 0.18], [0.6918, 0.78], [0.2993, 0.44], [0.5486, 0.66], [0.7848, 0.86], [0.9383, 0.98], [0.2292, 0.38], ] @@bad_tests = [1.02, -3]
def test_ok @@ok_tests.each do |val, exp| assert_equal(exp, rescale_price_fraction(val)) assert_equal(exp, Price.new(val).standard_value) end @@bad_tests.each do |val| assert_raise(ArgumentError) {rescale_price_fraction(val)} assert_raise(ArgumentError) {Price.new(val).standard_value} end end
# demonstrate why it's bad to use floats for money # 0.06 should map to 0.18, but the floating point representation of 0.06 is # slightly less, so the conversion returns 0.10 instead def test_uhoh assert_equal(0.18, Price.new(0.06).standard_value) end
end</lang>
output
Loaded suite price_fraction Started .F Finished in 0.018000 seconds. 1) Failure: test_uhoh(PriceFractionTests) [price_fraction.rb:119]: <0.18> expected but was <0.1>. 2 tests, 23 assertions, 1 failures, 0 errors, 0 skips
Tcl
Structured as two functions, one to parse the input data as described in the problem into a form which Tcl can work with easily, and the other to perform the mapping. <lang tcl># Used once to turn the table into a "nice" form proc parseTable table {
set map {} set LINE_RE {^ *>= *([0-9.]+) *< *([0-9.]+) *:= *([0-9.]+) *$} foreach line [split $table \n] {
if {[string trim $line] eq ""} continue if {[regexp $LINE_RE $line -> min max target]} { lappend map $min $max $target } else { error "invalid table format: $line" }
} return $map
}
- How to apply the "nice" table to a particular value
proc priceFraction {map value} {
foreach {minimum maximum target} $map {
if {$value >= $minimum && $value < $maximum} {return $target}
} # Failed to map; return the input return $value
}</lang> How it is used: <lang tcl># Make the mapping set inputTable {
>= 0.00 < 0.06 := 0.10 >= 0.06 < 0.11 := 0.18 >= 0.11 < 0.16 := 0.26 >= 0.16 < 0.21 := 0.32 >= 0.21 < 0.26 := 0.38 >= 0.26 < 0.31 := 0.44 >= 0.31 < 0.36 := 0.50 >= 0.36 < 0.41 := 0.54 >= 0.41 < 0.46 := 0.58 >= 0.46 < 0.51 := 0.62 >= 0.51 < 0.56 := 0.66 >= 0.56 < 0.61 := 0.70 >= 0.61 < 0.66 := 0.74 >= 0.66 < 0.71 := 0.78 >= 0.71 < 0.76 := 0.82 >= 0.76 < 0.81 := 0.86 >= 0.81 < 0.86 := 0.90 >= 0.86 < 0.91 := 0.94 >= 0.91 < 0.96 := 0.98 >= 0.96 < 1.01 := 1.00
} set map [parseTable $inputTable]
- Apply the mapping to some inputs (from the Oz example)
foreach example {.7388727 .8593103 .826687 .3444635 .0491907} {
puts "$example -> [priceFraction $map $example]"
}</lang> Output:
.7388727 -> 0.82 .8593103 -> 0.90 .826687 -> 0.90 .3444635 -> 0.50 .0491907 -> 0.10