5 Recipe for Intervals
When the domain information that needs to be represented in the program consists of one or more ranges of numbers, then the data definition is concerned with handling intervals. An example would be a taxation scheme in which there is no taxation for goods below 1000 €, moderate taxation between 1 k€ – 10 k€, and high taxation for goods above 10 k€. When dealing with intervals special care needs to be taken to treat the boundary cases correctly. In the example above, is a product of 1000 € still without taxation or does it fall in the next interval? Boundaries can be represented precisely in this way: [0, 1000), [1000, 10000), [10000, inf), where brackets [ and ] denote inclusive boundaries and parentheses ( and ) denote exclusive boundaries: 1000 does not belong to the interval [0, 1000), but 0 does.
Data definitions for intervals typically use symbols to name each interval as well as constants to define the boundaries of the intervals.
tax.pf serves as the example for intervals.t
5.1 Steps
5.1.1 Problem statement
Write down the problem statement as a comment. The problem statement should answer these questions: What information needs to be represented? What should the function (to be implemented) do with the data? What cases need to be considered?
#<
A fictitious country has decided to introduce a three-stage sales tax. Cheap items below 1 k€ are not taxed. Goods of more than 10 k€ are taxed at 10%. Items in between are taxed at a rate of 5%. Give a data definition and define a function that computes the amount of tax for a given item price.
>#
5.1.2 Data definition
# enumeration of tax stages:
# :no-tax, :low-tax, :high-tax
# :Int represents Euro
1000 :LOW-TAX-BOUNDARY! # interpret.: price in Euro
10000 :HIGH-TAX-BOUNDARY! # interpret.: price in Euro
5.1.3 Function name
sales-tax:
5.1.4 Parameter list
sales-tax: (price :Int -> :Int)
5.1.5 Function stub
sales-tax: (price :Int -> :Int) {
0
} fun
5.1.6 Purpose statement
Write down a purpose statement (given as a comment). The purpose statement should describe what the function computes (not how it does that) and should mention the given inputs and produced result.
# Returns the amount of tax for the given price.
5.1.7 Examples and expected results
Write down examples with expected results in the test function. The test examples should cover corner cases (e.g., boundary values) and a typical case of each category (e.g. a value from the interior of an interval). Boolean functions should test positive and negative examples. You may already define constants for use in the implementation. Check that the codeis parsed without errors. (Some tests will fail for the stub.)
Examples:
For a price of 0 € expect a sales tax of 0 €.
For a price of 537 € expect a sales tax of 0 €.
For a price of 1000 € expect a sales tax of 50 €.
For a price of 1282 € expect a sales tax of 64 €.
For a price of 10000 € expect a sales tax of 1000 €.
For a price of 12017 € expect a sales tax of 1202 €.
Corresponding test cases in test function:
sales-tax-test: {
0 sales-tax, 0, test=
537 sales-tax, 0, test=
1000 sales-tax, 1000 0.05 * round, test=
1282 sales-tax, 1282 0.05 * round, test=
10000 sales-tax, 10000 0.10 * round, test=
12017 sales-tax, 12017 0.10 * round, test=
} fun
sales-tax-test
The tax values are rounded and converted to integer numbers, which represent whole Euros.
Note that the test cases cover each of the boundary values of the intervals, and at least one value from the interior of each interval.
5.1.8 Function body
Implement the function body. Put required helper functions on a "wish list." These will be implemented later.
How to identify the need for a helper function: A function should perform one well-defined task. A change in task or data type should be outsourced in a helper function. Moreover, a reusable subtask should be outsourced in a helper function (Don’t Repeat Yourself, DRY principle). It is often helpful to write a stub for the helper functions. This way you can already run the program.
# Returns the amount of tax for the given price.
sales-tax: (price :Int -> :Int) { # :Int represents whole Euro
{ 0 price <= price 1000 < and } { # :no-tax interval
0
}
{ 1000 price <= price 10000 < and } { # :low-tax interval
price 0.05 * round
}
{ price 10000 >= } { # :high-tax interval
price 0.10 * round
}
{ true } { # error: if this line is reached then price < 0
"sales-tax, error: negative price" err
}
} cond-fun
The implementation reflects the structure of the data. As specified in the enumeration there are three cases: :no-tax, :low-tax, and :high-tax. These three cases correspond to three intervals: [0, 1000), [1000, 10000), and [1000, inf). For each interval there is a condition that matches one of the intervals. When formulating the conditions care has to be taken to handle the boundary values correctly. In addition, there is an error case for negative prices. Since Euro is represented as integer number, a negative number could be provided, which could lead to an error.
5.1.9 Testing
Check that the function body satisfies the tests. Correct the function body (and the tests). Look for opportunities to simplify the structure of the code. This typically requires multiple iterations.
The given implementation satisfies all test examples:
tax.pf, line 36: Check passed.
tax.pf, line 37: Check passed.
tax.pf, line 38: Check passed.
tax.pf, line 39: Check passed.
tax.pf, line 40: Check passed.
tax.pf, line 41: Check passed.
All 6 tests passed!
5.1.10 Review and revise
Review and revise the function name, the parameter names, and the purpose statement. Improve them if necessary. A design is not complete until it has a purpose statement and tests.
0.05 LOW-TAX-RATE!
0.10 HIGH-TAX-RATE!
# Returns the amount of tax for the given price.
sales-tax: (price :Int -> :Int) { # :Int represents whole Euro
{ price 0 < } { # error
"sales-tax, error: negative price" err
}
{ price LOW-TAX-BOUNDARY < } {
0
}
{ price HIGH-TAX-BOUNDARY < } {
price LOW-TAX-RATE * round
}
{ true } {
price HIGH-TAX-RATE * round
}
} cond-fun
Domain knowledge might suggest to include a constant for the low-taxation category as well. There could be a revision of the taxation system, in which the first interval of [0, 1000) Euro becomes subject to a small taxation rate as well. Moreover, an additional interval for higher-priced items could be introduced, which would require adding a case. It is up to the programmer’s judgment to decide how far the generalization should go, because this can come at a cost. For example, introducing another constant for the first interval could increase the effort to read the code.
If we are satisfied with the function, we can write a reusable template, which can serve as a basis when implementing functions related to this taxation scheme in the future.
0.05 LOW-TAX-RATE!
0.10 HIGH-TAX-RATE!
fn-for-tax: (price :Int -> ...) { # :Int represents whole Euro
{ price 0 < } { # error
"sales-tax, error: negative price" err
}
{ price LOW-TAX-BOUNDARY < } {
...
}
{ price HIGH-TAX-BOUNDARY < } {
...
}
{ true } {
...
}
} cond-fun
This template can be adapted for new functions that operate on this data type.
5.2 Exercises
A taxation rate of 1.5% is introduced for items below 1000 €. Modify the sales-tax function to reflect this change.
A revision of the taxation system introduces a linearly increasing taxation rate for the middle interval (1 k€ – 10 k€). As before, the taxation rate is 5% at 1 k€ and 10% at 10 k€, but is linearly interpolated in between. Modify the sales-tax function to reflect this change.