COBOL inspired numeric formatting for Eiffel

by Finnian Reilly (modified: 2023 Dec 08)

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:

  1. EL_FORMAT_DOUBLE
  2. EL_FORMAT_INTEGER

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