MODULE 7 · WORKING WITH TEXT · 7/8

Parsing CSV and NUMVAL

15 min30 XPExercise

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), display REJECTED: 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 (via WS-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
fixed format
Run your program to see its output here. The first visible test's input and datasets are used.
Submit to grade your program against every test.