Parsing CSV and NUMVAL
A comma-separated line is just text. To do arithmetic on its numbers you need two steps: split the line into alphanumeric fields with UNSTRING, then convert the numeric-looking text into real numbers.
Why not UNSTRING straight into a number?
UNSTRING into a numeric field is only defined for plain digits, as you
saw with dates. The standard treats the piece as an unsigned whole number,
so a decimal point, minus sign or currency symbol makes it invalid data.
Some compilers (GnuCOBOL included) make a sensible guess, others give you
garbage or an abend. Don't rely on either: split into PIC X fields and
convert with a function.
NUMVAL
FUNCTION NUMVAL(text) reads text such as "12.50", " -42.5 " or
"4" and returns its numeric value. Leading and trailing spaces, a sign
and a decimal point are all allowed.
IDENTIFICATION DIVISION.
PROGRAM-ID. CSVLINE.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-LINE PIC X(40) VALUE "P100,Widget,12.50,4".
01 WS-CODE PIC X(4).
01 WS-DESC PIC X(15).
01 WS-PRICE-TX PIC X(10).
01 WS-QTY-TX PIC X(5).
01 WS-PRICE PIC 9(5)V99.
01 WS-QTY PIC 9(3).
01 WS-VALUE PIC 9(7)V99.
01 WS-VALUE-ED PIC Z(6)9.99.
PROCEDURE DIVISION.
UNSTRING WS-LINE DELIMITED BY ","
INTO WS-CODE WS-DESC WS-PRICE-TX WS-QTY-TX
END-UNSTRING
COMPUTE WS-PRICE = FUNCTION NUMVAL(WS-PRICE-TX)
COMPUTE WS-QTY = FUNCTION NUMVAL(WS-QTY-TX)
COMPUTE WS-VALUE = WS-PRICE * WS-QTY
MOVE WS-VALUE TO WS-VALUE-ED
DISPLAY WS-CODE " " WS-DESC " " WS-VALUE-ED
STOP RUN.
Output:
P100 Widget 50.00
NUMVAL-C
FUNCTION NUMVAL-C also accepts a currency sign and thousands separators,
and treats a trailing CR or DB as negative. It is the one to use for
amounts formatted for humans:
IDENTIFICATION DIVISION.
PROGRAM-ID. NUMVALS.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-AMT PIC S9(7)V99.
01 WS-AMT-ED PIC -(7)9.99.
PROCEDURE DIVISION.
COMPUTE WS-AMT = FUNCTION NUMVAL(" -42.5 ")
MOVE WS-AMT TO WS-AMT-ED
DISPLAY "NUMVAL: " WS-AMT-ED
COMPUTE WS-AMT = FUNCTION NUMVAL-C("$1,234.56")
MOVE WS-AMT TO WS-AMT-ED
DISPLAY "NUMVAL-C: " WS-AMT-ED
COMPUTE WS-AMT = FUNCTION NUMVAL-C("1,000.00CR")
MOVE WS-AMT TO WS-AMT-ED
DISPLAY "CREDIT: " WS-AMT-ED
STOP RUN.
Output:
NUMVAL: -42.50
NUMVAL-C: 1234.56
CREDIT: -1000.00
Validate before you convert
NUMVAL assumes its argument is valid. Given "12.5O" (letter O, not
zero), what you get back depends on the compiler: GnuCOBOL quietly returns
the digits it managed to read, other compilers may fail. Never trust input
from outside. TEST-NUMVAL (and TEST-NUMVAL-C) check the text
first. They return 0 if it is valid, otherwise the position of the first
bad character. Empty or all-space text is invalid too.
IDENTIFICATION DIVISION.
PROGRAM-ID. VALIDATE.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-TEXT PIC X(10).
01 WS-BAD-POS PIC 9(2).
PROCEDURE DIVISION.
MOVE "12.5O" TO WS-TEXT
MOVE FUNCTION TEST-NUMVAL(WS-TEXT) TO WS-BAD-POS
IF WS-BAD-POS = 0
DISPLAY "OK: " WS-TEXT
ELSE
DISPLAY "BAD CHARACTER AT " WS-BAD-POS ": " WS-TEXT
END-IF
STOP RUN.
Output: BAD CHARACTER AT 05: 12.5O.
Limits of simple CSV parsing
DELIMITED BY "," is fine as long as no field contains a comma. Real CSV
allows quoted fields such as "Hopper, Grace", and then a plain UNSTRING
splits in the wrong place. Handling quotes means walking the line
character by character with reference modification. In practice, most
shops agree a file format with the sender that avoids the problem: no
commas in values, or a different delimiter such as |.
Also watch for empty fields: P100,,12.50,4 gives an all-space
description. Check required fields are not SPACES before using them.
On the job
Treat every field from an external feed as text until it has passed
validation. A classic production abend (a S0C7 data exception) is
a program doing arithmetic on a field that holds spaces or letters.
Validating up front and rejecting the record with a clear message is
far cheaper than a 3 a.m. call about a failed batch run.
Your task
Staff expense claims arrive as CSV lines:
EMPLOYEE-ID,CATEGORY,AMOUNT
for example E1001,TRAVEL,$245.50. The amount may or may not have a $
sign and spaces around it. Read lines until one says END.
For each line, split it into WS-EMP-ID, WS-CATEGORY and
WS-AMOUNT-TX, then:
- if the amount text is not a valid currency amount (use
TEST-NUMVAL-C), displayREJECTED:followed by the whole line; - otherwise convert it with
NUMVAL-C, add it to the total, count the claim, and display the ID, category and amount (viaWS-AMOUNT-ED) separated by single spaces.
After END, display the count and total:
E1001 TRAVEL 245.50
E1002 MEALS 32.00
E1001 HOTEL 1210.00
CLAIMS: 03
TOTAL: 1487.50