COBOL inspired numeric formatting for Eiffel
Introduction
Out of curiosity I have been looking at some youtube videos about that strange coding language from the 1950's known as COBOL that is still in widespread use today. I noticed in a code listing something like this:
PIC 999V99
This represents a five-digit number with an implied decimal; three of the digits are left of the decimal point, and two are post-decimal digits.
I have termed this "likeness-formatting" as the format string has a likeness to applied output.
I like this visual style of formatting and wondered if it could be applied to extended versions of the Eiffel base classes FORMAT_DOUBLE and FORMAT_INTEGER.
Abstract class EL_FORMAT_LIKENESS
Class EL_FORMAT_LIKENESS is an abstract ancestor for classes:
The functioning of these classes can be easily understood by examining the following autotest routines.
Autotest for EL_FORMAT_DOUBLE
test_format_double
local
double: EL_FORMAT_DOUBLE; pi: DOUBLE
format_table: EL_HASH_TABLE [STRING, STRING]
do
pi := {MATH_CONST}.Pi
create format_table.make (<<
["99.99", "3.14"], -- width = 5, decimals = 2
["99,99", "3,14"], -- decimal point is a comma
["99.99%%", "3.14%%"], -- percentile
["99.99|", " 3.14"], -- right justified
["|99.99", "3.14 "], -- left justified
["|999.99|", " 3.14 "], -- centered and width = 6
["|99.99%%", "3.14%% "] -- left justified percentile
>>)
double := "99.99"
assert_same_string (Void, double.formatted (pi * 100), "314.16")
across format_table as table loop
double := table.key
if double.formatted (pi) /~ table.item then
lio.put_string_field (table.key, table.item)
lio.put_new_line
lio.put_double_field ("formatted", pi, table.key)
lio.put_new_line
assert ("same as formatted", False)
end
end
end
Things to note in listing
- The double variable is initialised by string conversion
- The vertical bar character | is used to represent justification in a visual manner
- The put_double_field routine for the lio class now has an extra optional argument specifying the format
Autotest for EL_FORMAT_INTEGER
As an aside, class EL_FORMAT_INTEGER also has the ability to spell numbers from 1 to 100 in English.
test_format_integer
local
integer: EL_FORMAT_INTEGER; format_table: EL_HASH_TABLE [STRING, STRING]
n: INTEGER
do
n := 64
create format_table.make (<<
["999", "64"], -- width = 3
["|999", "64 "], -- left justified
["999|", " 64"], -- right justified
["0999|", "064"], -- left justified with zero padding
["|9999|", " 64 "], -- centered
["999%%|", " 64%%"], -- percentile
["|999%%", "64%% "] -- left justified percentile
>>)
across format_table as table loop
integer := table.key
if integer.formatted (n) /~ table.item then
lio.put_string_field (table.key, table.item)
lio.put_new_line
lio.put_string_field ("formatted", integer.formatted (n))
lio.put_new_line
assert ("same as formatted", False)
end
end
end
Things to note in listing
A preceding '0' character indicates to use zeros as padding
Helper Classes
EL_SHARED_FORMAT_FACTORY
Class EL_SHARED_FORMAT_FACTORY provides access to EL_FORMAT_FACTORY which provides cached instances of EL_FORMAT_DOUBLE or EL_FORMAT_INTEGER with a likeness specifier argument.
EL_SIMPLE_IMMUTABLE_PARSER_8
Class EL_SIMPLE_IMMUTABLE_PARSER_8 was create specifically for this project and as a general aid to performing simple parsing operations. This code listing illustrates it's use. For efficiency it uses the {IMMUTABLE_STRING_8}.shared_substring routine.
class
EL_FORMAT_LIKENESS
feature {NONE} -- Initialization
make (likeness: STRING)
local
parser: EL_SIMPLE_IMMUTABLE_PARSER_8; decimal_point: CHARACTER_REF
decimal_count: INTEGER; justify_right, justify_left, zero_pad: BOOLEAN
l_width: INTEGER
do
parser := likeness
create decimal_point
parser.try_remove_right_character ('|')
justify_right := parser.was_removed
parser.try_remove_left_character ('|')
justify_left := parser.was_removed
parser.try_remove_right_character ('%%')
is_percentile := parser.was_removed
parser.try_remove_left_character ('0')
zero_pad := parser.was_removed
decimal_count := parsed_decimal_count (parser, decimal_point)
l_width := parser.target.count
l_width := l_width + decimal_count + decimal_count.to_boolean.to_integer
make_sized (l_width, decimal_count)
set_decimal_point (decimal_point.item)
if justify_left and justify_right then
center_justify
elseif justify_left then
left_justify
elseif justify_right then
right_justify
else
no_justify
end
if right_justified and zero_pad then
zero_fill
end
end
end
Class EL_FORMAT_DOUBLE is also a client of EL_SIMPLE_IMMUTABLE_PARSER_8 with this one routine
class
EL_FORMAT_DOUBLE
inherit
FORMAT_DOUBLE
feature {NONE} -- Implementation
parsed_decimal_count (parser: EL_SIMPLE_IMMUTABLE_PARSER_8; decimal_point: CHARACTER_REF): INTEGER
do
parser.reset_count_removed
across ".," as c until parser.was_removed loop
decimal_point.set_item (c.item)
parser.try_remove_right_until (decimal_point.item)
if parser.was_removed then
Result := parser.count_removed - 1
end
end
end
end