#! /usr/bin/gawk -f # Last edited on 2008-06-29 23:08:36 by stolfi # Functions for handling plain-text tables. # TABLE FORMAT PARAMETERS # # The format of a table is determined by the following global variables: # # {frsep} Fraction separator (empty, or a single char not in [-+0-9|! ]). # {thsep} Thousands separator (empty, or a single char not in [-+0-9|!]). # {altzero} Alternate representation of 0 (empty, or a string not containing [+1-9|!]). # # TABLE LINE FORMAT # # Any input line is first broken into three parts: the /margin/, the # /body/, and the /comment/. If the line contains a '#' character, # the comment part consists of all blanks that precede the '#', the # '#' itself, and everything that follows the '#'. Otherwise the # comment part consists of all trailing blanks in the line. The # margin then consists of the leading blanks that are not part of # the comment. The body is what is left. # # The body of the line is then classified into four line # types, each identified by a one-character /tag/: # # a /blank line/, if the body is empty (tag = ' '). # # a /rule line/, if its non-blank characters are all '-' or '+' (tag = '+'); # # a /header line/, if it contains at least one '!' and no '|' (tag = '!') # # an /info line/, otherwise (tag = '|'); # # A line that contains both '!' and '|' is invalid. # # TABLE FIELDS # # Each line of the input contains zero or more /fields/. A blank # line has zero fields; otherwise, the occurrences of the tag in the # line's body separate it into one or more fields. Leading and # trailing blanks in each field are discarded; so a field which is all # blanks is assumed to be empty. # # Fields are numbered from left to right, starting at 1. # # Note that if the body begins (resp. ends) with the tag character, # the first (resp. last) field of that line will be empty. Note also # that one cannot have a header line with exactly one field, because # it will be parsed as an info line. # # TABLE ROWS AND COLUMNS # # A table is set of lines (/rows/) such that all non-blank # lines have the same number of fields. # # Thus, each table either has only blank lines, or has a well-defined # and positive number of columns, and a positive number of non-blank # rows. The columns are numbered from left to right, starting at 1. # # COLUMN NAMES # # If the table has a header row, each field of that row is taken # as the /name/ of that column, provided it is non-empty and # distinct from all previous headers. Otherwise the column remains # nameless. (Column names are relevant only for some programs, e.g. # {table-join}.) # # NUMERIC AND ALPHABETIC FIELDS # # A field of an info row is considered /numeric/ if it is non-empty # and consists of an optional sign, followed by one or more decimal # digits, possibly with {frsep} (decimal point) and/or {thsep} # (thousands separator) characters. The {thsep} character may appear # only between two digits. The /numeric value/ of such a field is # obtained by removing any {thsep} characters and replacing the # {frsep} character by ".". The {altzero} string, if not empty, and a # single {frsep} are also accepted as numeric fields, with numeric # value 0. A field is considered alphabetic if it is non-empty and # not numeric. # # The {frsep} and {thsep} parameters must be empty strings or # distinct single characters, not in [-+0123456789|!]. The {frsep} # character must be non-blank. If {frsep} is empty, numeric fields # may not have fractional parts. If {thsep} is empty, numric fields # may not have thousands-separators. The {altzero} string # must not contain [+123456789|!] (but may contain '-' and/or '0', # or embedded blanks). # # NUMERIC AND ALPHABETIC COLUMNS # # Each column of a table then classified as /numeric/ or /alphabetic/. # A column is assumed to be numeric if its info rows contain # at least one numeric field and no alphabetic fields. Note that # any non-empty, non-numeric field in an info row marks the # the whole column as alphabetic. # # For each numeric column, the program also defines the /precision/ # as being the maximum number of digits after the decimal point in # any info field, or -1 if no info field has an explicit decimal point. # It also defines a /thousands-flag/, which is true if # and only if {thsep} is not empty and the column contains an # info field with thousands-separators. It also defines # the /plus-flag/, if any info field in the column has an # explicit '+' sign. # # (SUB) TOTAL ROWS # # A /total row/ is an info row that contains the string "TOTAL", # "total", or "Total" as one of its fields, prefixed by zero or more # instances of "SUB", "Sub", or "sub" (with or without joining # hyphens). The number of such prefixes is the /level/ of that total # row. # # A total row is /consistent/ if the numeric value of every field of # that row that belongs to a numeric column is equal to the sum of # the values of all fields in that column that are not total rows and # lie strictly between that row and the previous total row with the # same or lower level (or the top of the table if there is no such # previous total row). # # CANONICAL FIELD FORMAT # # To print a table in its /canonical format/, every non-empty field # in an info row and a numeric column is replaced by its numeric value, # converted to a string according to the column's precision # and thousands-flag attributes. Then, every non-empty field in an # info or header row is padded with an extra blank at the left (except # for the first field of the row) and one extra blank at the right # (except for the last field). # # NUMERIC FIELD FORMATTING # # When converting the numeric value 0 to its canonical representation, # if the {altzero} parameter string is not empty, the result is the # {altzero} string followed by {max(0,prec)} blanks, where {prec} is the # column's precision; otherwise, if {prec} is not -1, it is a single # {frsep} followed by {prec} blanks; otherwise it is just "0". # # When converting a nonzero numeric value to its canonical # representation, the value is first printed with {sprintf} using "%d" # or "%+d" format if {prec} is -1, or "%#.{prec}f" format or # "#+.{prec}f" format if {prec} is non-negative; where the '+' form is # used iff the column's plus-flag is set. Then, if {thsep} is # non-empty and the column's thousands-flag is set, the character # {thsep} is inserted to separate the digits of the integer and fraction # parts in groups of three, starting at the fraction point. # # COLUMN WIDTHS # # The /column width/ of each column is defined as the maximum # length of any of its fields, including info, header, and rule # rows. The /table margin/ is also defined as the shortest margin # of any non-blank row in the table. # # COLUMN WIDTH REGULARIZATION # # To /regularize/ a column, every field in it extended so as to match # the column width {wd}. In a rule row, that means replacing the # field by a string of {wd} '-'s. In a a header or info row, that # means extending with blanks until its length is {wd}; the blanks are # added at the left in numeric columns, and at the right in # non-numeric columns. function txtable_argument_parse() { # Validates the global parameters {frsep,thsep,altzero} specified # in the command line. if ((length(frsep) > 1) || (frsep ~ /[-+0-9 |!]/)) { arg_error(("invalid parameter {frsep} = \"" frsep "\"")); } if ((length(thsep) > 1) || (thsep ~ /[-+0-9 |!]/) || (thsep == frsep)) { arg_error(("invalid parameter {thsep} = \"" thsep "\"")); } if ((altzero ~ /[+1-9|!]/) || (altzero ~ /^[ ]/) || (altzero ~ /[ ]$/)) { arg_error(("invalid parameter {altzero} = \"" altzero "\"")); } } function txtable_untabify(lin, k,nb) { # Removes tabs and other crud from the line {lin}, preserving its visual appearance. gsub(/[\240\014]/, " ", lin); gsub(/[\015]/, "", lin); while ((k = index(lin, "\011")) > 0) { nb = 8 - ((k-1) % 8); lin = ( substr(lin,1,k-1) sprintf("%*s", nb, "") substr(lin,k+1) ); } return lin; } function txtable_split_line(lin) { # Removes TABs and other garbage from line {lin}, then splits # into its main parts {row_tag,row_ind,row_nf,row_fld[1..row_nf],row_cmt}. # Does not strip blanks from the fields. # Cleanup: lin = txtable_untabify(lin); # Split out the {row_cmt}: match(lin, /[ ]*([\#].*|)$/); if (RSTART <= 0) { prog_error(("duh?")); } row_cmt = substr(lin, RSTART, RLENGTH); lin = substr(lin, 1, RSTART-1); if (lin == "") { # Blank line: row_ind = 0; row_tag = " "; row_nf = 0; split("", row_fld); } else { # Non-blank line. # Split out the {row_ind}: match(lin, /^[ ]*/); if (RSTART != 1) { prog_error(("deh?")); } row_ind = RLENGTH; lin = substr(lin, RSTART + RLENGTH); if (lin == "") { prog_error(("boh?")); } if (lin ~ /^[-+ ]*$/) { # Assume that it is a rule line: row_tag = "+"; } else if (lin ~ /[!]/) { # Assume that it is a header line: if (lin ~ /[|]/) { data_error(("ambiguous header/info row")); } row_tag = "!"; } else { # Assume that it is an info line: row_tag = "|"; } # Break into fields: row_nf = split(lin, row_fld, ("[" row_tag "]")); if (row_nf < 1) { prog_error(("beh?")); } } } function txtable_clear() { # Sets the current table to an empty table. # The raw table is stored in the global arrays {tbl_tag,tbl_fld,tbl_cmt} # and in the global variables {tbl_nrows,tbl_ncols,tbl_ind}. # Does not disturb the current line $0. # Raw table data: tbl_nrows = 0; # Number of table rows in current table. tbl_ncols = 0; # Number of colunms in table (0 if all rows are blank). tbl_ind = -1; # Table indentation (-1 if all rows are blank). # Row attributes (indexed by row index {i} in {1..tbl_nrows}): split("", tbl_tag); # The tags of the rows are {tbl_tag[1..nrows]} (" ", "|", "!", or "+"). split("", tbl_cmt); # The comment parts of the rows are {tbl_cmt[1..nrows]}. # Field attributes (indexed by {i,j} in {1..tbl_nrows,1..tbl_ncols}): split("", tbl_fld); # The text fields of the current table are {tbl_fld[1..nrows,1..ncols]}. } function txtable_save_line(row_ind,row_tag,row_nf,row_fld,row_cmt, i,j) { # Appends a new row to the current table. # If the row is not blank, sets/checks {tbl_ncols} from {row_nf}. # The row data is in the parameters {row_ind,row_tag,row_nf,row_fld,row_cmt}, # as defined by {txtable_line_split()}. # One more row: tbl_nrows++; i = tbl_nrows; # Check tag, set/check {tbl_ncols}, update {tbl_ind}: if (row_tag == " ") { # Blank row, must have zero fields and zero indent: if (row_nf != 0) { prog_error(("xii!")); } if (row_ind != 0) { prog_error(("xoo!")); } } else if ((row_tag == "+") || (row_tag == "|") || (row_tag == "!")) { # Non-blank row, must have at least one field: if (row_nf <= 0) { prog_error(("xee!")); } if (tbl_ncols == 0) { # First non-blank row: tbl_ncols = row_nf; tbl_ind = row_ind; } else { if (tbl_ncols != row_nf) { data_error(("inconsistent number of columns = " tbl_ncols " " row_nf "")); } # Remember the smallest non-blank row indentation as the table's indentation: if (row_ind < tbl_ind) { tbl_ind = row_ind; } } } else { # Unexpected tag: prog_error(("noo!")); } tbl_tag[i] = row_tag; # Store fields: i = tbl_nrows; for (j = 1; j <= row_nf; j++) { tbl_fld[i,j] = row_fld[j]; } # Store comment: tbl_cmt[i] = row_cmt; } function txtable_get_column_attributes(frsep,thsep,altzero, i,j,xvij,n,nnum,nalf,vij,yvij,pr,th,sg) { # Determines the column attributes {tbl_nump[j],tbl_prec[j],tbl_thfg[j],tbl_psfg[j],tbl_cwd[j]} # and the numeric values of every field in non-blank rows and numeric columns. # Column attributes (indexed by column index {j} in {1..tbl_ncols}): split("", tbl_cwd); # The max width of any field in the column. split("", tbl_nump); # True iff column {j} is numeric. split("", tbl_prec); # Max precision of info fields, or -1 if all integers. split("", tbl_thfg); # True iff any info field had the {thsep}. split("", tbl_psfg); # True iff any positive info field had an explicit plus sign. # Numeric values, only for info fields (indexed by {i,j} in {1..tbl_nrows,1..tbl_ncols}): split("", tbl_val); # Numeric value of field ("*" if not numeric). for (j = 1; j <= tbl_ncols; j++) { # Get numeric values on column {j}, count numeric and non-numeric fields: nnum = 0; nalf = 0; tbl_cwd[j] = 0; for (i = 1; i <= tbl_nrows; i++) { if (tbl_tag[i] != " ") { # Non-blank row; get the field and update the column width: xvij = tbl_fld[i,j]; n = length(xvij); if (n > tbl_cwd[j]) { tbl_cwd[j] = n; } if (tbl_tag[i] == "|") { # Info line, try to get the numeric value: if (xvij == "") { # Empty field -- assign 0 value, just in case: tbl_val[i,j] = 0; # Count it as neither numeric nor alphabetic. } else { # Non-empty field -- try to get numeric value: vij = txtable_numeric_value(xvij,frsep,thsep,altzero); tbl_val[i,j] = vij; # Tally numeric and alphabetic fields: if (vij != "*") { nnum++; } else { nalf++; } } } } } # Decide whether column is numeric or alphabetic: tbl_nump[j] = ((nnum > 0) && (nalf == 0)); if (tbl_nump[j]) { # Determine {tbl_prec[j],tbl_thfg[j],tbl_psfg[j]}: tbl_prec[j] = -1; tbl_thfg[j] = 0; tbl_psfg[j] = 0; for (i = 1; i <= tbl_nrows; i++) { if (tbl_tag[i] == "|") { # Info line: xvij = tbl_fld[i,j]; vij = tbl_val[i,j]; if (xvij != altzero) { # Remove the thousands separators, if any: yvij = txtable_strip_thsep(xvij,thsep); # If there were any, mark the column as needing them: th = (yvij != xvij); if (th) { tbl_thfg[j] = 1; } # Get the precision {pr}, or -1 if integer: pr = txtable_get_precision(yvij,frsep); if (pr > tbl_prec[j]) { tbl_prec[j] = pr; } # Check whether there is a '+' on a nonzero value: ps = ((vij != 0) && (yvij ~/^[+]/)) if (ps) { tbl_psfg[j] = 1; } # printf "«%s» («%s»)", xvij, yvij > "/dev/stderr"; # printf " pr = %d th = %d ps = %d\n", pr, th, ps > "/dev/stderr"; } } } } # Debugging printouts: # printf "column %2d", j > "/dev/stderr"; # printf " nump = %d", tbl_nump[j] > "/dev/stderr"; # printf " prec = %2d", tbl_prec[j] > "/dev/stderr"; # printf " thfg = %d", tbl_thfg[j] > "/dev/stderr"; # printf " psfg = %d", tbl_psfg[j] > "/dev/stderr"; # printf " cwd = %d\n", tbl_cwd[j] > "/dev/stderr"; } } function txtable_recompute_and_reformat_values(frsep,thsep,altzero, i,j,xvij,lev,n,sum,k,pr,th,ps) { # In (sub)total rows, recomputes the numeric values of numeric columns by adding # the previous rows. Then reconstructs all fields in numeric columns and info rows # from their numeric values, according to the column attributes, and strips # leading and trailing blanks from all other fields. Adds one blank around # all header and info fields (except at table edges) and recomputes the column widths. # Does not extend the fields to the uniform column width. # Row attributes (indexed by row index {i} in {1..tbl_nrows}): split("", tbl_tlv); # The row level if total row, or -1 if ordinary row. for (i = 1; i <= tbl_nrows; i++) { tbl_tlv[i] = -1; # Ordinary by default; if (tbl_tag[i] == "|") { # Info row -- set {tbl_tlv[i] >= 0} iff any field is "total", "subtotal", etc. for (j = 1; j <= tbl_ncols; j++) { xvij = tbl_fld[i,j]; lev = txtable_field_tot_level(xvij); if (lev != -1) { tbl_tlv[i] = lev; break; } } } if (tbl_tag[i] != " ") { # Non-blank row -- reformat all fields (recomputing totals, if any): for (j = 1; j <= tbl_ncols; j++) { printf "«%s» = %s", tbl_fld[i,j], tbl_val[i,j] > "/dev/stderr"; # Reformat the element, without any padding: if ((tbl_nump[j]) && (tbl_tag[i] == "|")) { # Numeric column in info row. # Recompute field if needed: if (tbl_tlv[i] >= 0) { # (Sub)total row, must replace field by sum of previous rows: tbl_val[i,j] = txtable_recompute_total(i,j); } # Reformat the value: pr = tbl_prec[j]; th = tbl_thfg[j]; ps = tbl_psfg[j]; xvij = txtable_format_value(tbl_val[i,j],pr,th,ps,frsep,thsep,altzero); } else { # Strip surrounding blanks: xvij = tbl_fld[i,j]; gsub(/^[ ]+/, "", xvij); gsub(/[ ]+$/, "", xvij); } # Add padding where needed: if ((xvij != "") && (tbl_tag[i] != "+")) { # Header or info row, add padding blanks except at table edges: if (j > 1) { xvij = (" " xvij); } if (j < tbl_ncols) { xvij = (xvij " "); } } # Store the reformatted field: tbl_fld[i,j] = xvij; printf " --> «%s»\n", tbl_fld[i,j] > "/dev/stderr"; # Update the column width: n = length(xvij); if (n > tbl_cwd[j]) { tbl_cwd[j] = n; } } } } } function txtable_field_tot_level(xv, k) { # If field {xv} is "total", "subtotal", "subsubtotal", etc. # with various capitalizations, hyphens or underscores, # returns the number {k} of "sub"prefixes. # Otherwise returns -1. # Strip any leading blanks: gsub(/^[ ]+/, "", xv); # Count "sub" prefixes into {k}: k = 0; while (match(xv, /^(SUB|Sub|sub)[-_]?/)) { k++; xv = substr(xv, RSTART+RLENGTH); } if (xv ~ /^(TOTAL|Total|total)[ ]*$/) { return k; } else { return -1; } } function txtable_recompute_total(i,j, k,sum) { # Computes the sum of the numeric values of all elements # in column {j} that are in non-total rows above # row {i}, up to the first total line with level # less than or equal to that of row {i}. if (tbl_tlev[i] < 0) { prog_error(("ahh!")); } sum = 0; for (k = i-1; k >= 1; k--) { if (tbl_tlv[k] == -1) { # Ordinary line, accumulate it: sum += tbl_val[k,j]; } else if (tbl_tlv[k] <= tbl_tlv[i]) { # (Sub)total line of same or lower level, stop: break; } else { # (Sub)total line of higher level, ignore: } } return sum; } function txtable_print( i,j,xvij,wdij,dashes) { # Prints the table. The field in row {i} colum {j} is taken from # {tbl_fld[i,j]}. If {tbl_cwd[j]} is set, extends each field in that # column to that width. Does not use any of the tables. # {tbl_val,tbl_nump,tbl_prec,tbl_thfg,tbl_psfg,tbl_tlv} dashes = "-------------"; # A handful of dashes. for (i = 1; i <= tbl_nrows; i++) { if (tbl_tag[i] != " ") { # Print the table's indentation: if (tbl_ind > 0) { printf "%*s", tbl_ind, ""; } # Print the table fields: for (j = 1; j <= tbl_ncols; j++) { # Print the separator between columns: if (j > 1) { printf "%s", tbl_tag[i]; } # Print the field: xvij = tbl_fld[i,j]; wdij = tbl_cwd[j]; if (tbl_tag[i] == "+") { # Replace field by a string of '-'s: while (length(dashes) < wdij) { dashes = (dashes "--------"); } printf "%s", substr(dashes, 1, wdij); } else if (tbl_nump[j]) { # Pad at left, even the header: printf "%*s", wdij, xvij; } else { # Pad at right: printf "%*s", -wdij, xvij; } } } # Print the row's comment: printf "%s\n", tbl_cmt[i]; } } function txtable_numeric_value(xv,frsep,thsep,altzero, k,xt,a,b,az) { # Returns the numeric value of {xv}, or "*" if # {xv} is not a well-formed number. # Assume that {xv} is non-blank. # Strip leading ad trailing blanks: gsub(/^[ \011]+/, "", xv); gsub(/[ \011]+$/, "", xv); if (xv !~ /[0-9]/) { # Field contains no digits: return "*"; } else if ((altzero != "") && (xv == altzero)) { # Field is the alternate zero: return 0; } else { # May be be fractional number, possibly with {thsep,frsep}. if (thsep != "") { # Remove any {thsep} present in {xv}: while (1) { k = index(xv, thsep); if (k <= 0) { break; } # Check whether the {thsep} is between two digits: if ((k == 1) || (k == length(xv))) { return "*"; } a = substr(xv,k-1,1); b = substr(xv,k+1,1) if ((a < "0") || (a > "9") || (b < "0") || (b > "9")) { return "*"; } # OK, cut it out and retry: xv = (substr(xv, 1, k-1) substr(xv, k+1)); } } if (frsep == "") { # There must be no fraction point: if (index(xv, ".") > 0) { return "*"; } } else if (frsep != ".") { # Make sure that the fraction separator, if any, is "." # There must be no "." already: if (index(xv, ".") > 0) { return "*"; } # Change the {frsep}, if present, to ".": k = index(xv, frsep); if (k > 0) { xv = (substr(xv, 1, k-1) "." substr(xv, k+1)); } } # Now parse as an US-style number: if (xv == ".") { # Just "." (withou sign): return 0; } else if (xv ~ /^[-+]?[0-9]+([.][0-9]*|)$/) { # Sign, nonempty integer, optional "." and fraction. return xv + 0; } else if (xv ~ /^[-+]?[.][0-9]+$/) { # Sign, ".", nonempty fraction. return xv + 0; } else { return "*"; } } } function txtable_format_value(v,pr,th,ps,frsep,thsep,altzero, xv,k) { # Formats the numeric value {v} with precision {pr}, # thousands-flag {th}, plus-flag {ps}. Uses # {frsep} as fraction separator, {thsep} as thousands-separator, # {altzero} as an alternate form of zero. #Make sure that {v} is numeric: v += 0; if (v == 0) { # Value is zero, special handling: if (altzero != "") { # Use {altzero} with {max(0,pr)} blanks: xv = altzero; if (pr < 0) { return xv; } } else if (pr < 0) { # Integer column, use just "0": return "0"; } else { # Fractional column, use {frsep} with {pr} blanks: if (frsep == "") { prog_error(("wee!")); } xv = frsep; } if (pr > 0) { xv = (xv sprintf("%*s", pr,"")); } return xv; } else { # Value is nonzero: if (pr < 0) { # Format the number in "d" format. xv = sprintf((ps ? "%+d" : "%d"), v); } else { if (frsep == "") { prog_error(("woo!")); } # Format the number in "f" format with {pr} decimals: xv = sprintf((ps ? "#+.*f": "%.*f"), pr, v); } # If all is well, {pr} should be large enough to represent {v} without any rounding: if ((xv+0) != v) { prog_error(("unexpected rounding of \"" v "\" to \"" xv "\"")); } if ((pr >= 0) && (frsep != "") && (frsep != ".")) { # Replace the "." by {frsep}: k = index(xv, "."); if (k < 1) { prog_error(("eps!")); } xv = (substr(xv, 1, k-1) frsep substr(xv, k+1)); } if (th){ xv = txtable_insert_thsep(xv,pr,thsep); } } return xv; } function txtable_insert_thsep(xv,pr,ch, n,ip,pt,fp) { # Assumes that {xv} is a number with precision {pr}, with some # unknown fraction separator and no thousands-separator. # Inserts the character {ch} (which must be non-digit and non-empty) # between the integer and fraction digits of {xv} at every three digits # from the decimal point position. if (ch == "") { prog_error(("ulp!")); } if (pr < 0) { return txtable_insert_thsep_int(xv,ch); } else { n = length(xv); ip = substr(xv, 1, n - pr - 1); pt = substr(xv, n - pr, 1); fp = substr(xv, n - pr + 1); return (txtable_insert_thsep_int(ip,ch) pt txtable_insert_thsep_frac(fp,ch)); } } function txtable_insert_thsep_int(ip,ch, oip) { # Inserts the thousands-separator {ch} (which must be non-digit and non-empty) # in the integer part {ip} of a number: do { oip = ip; ip = gensub(/^([-+]?[0-9]+)([0-9][0-9][0-9])($|[^0-9])/, "\\1" ch "\\2\\3", "1", ip); } while (oip != ip); return ip; } function txtable_insert_thsep_frac(fp,ch, ofp) { # Inserts the thousands-separator {ch} (which must be non-digit and non-empty) # in the fraction part {fp} of a number: do { ofp = fp; fp = gensub(/(^|[^0-9])([0-9][0-9][0-9])([0-9]+)$/, "\\1\\2" ch "\\3", "1", fp); } while (ofp != fp); return fp; } function txtable_strip_thsep(xv,thsep, k) { # Removes all occurrences of {thsep} from {xv}. # If {thsep} is not empty, it must be a single character. if (thsep == "") { return xv; } if (length(thsep) != 1) { prog_error(("eek!")); } while ((k = index(xv, thsep)) > 0) { xv = (substr(xv, 1, k-1) substr(xv, k+1)); } return xv; } function txtable_get_precision(xv,frsep, k,n) { # Obtains the precision of a number {xv}, namely the # number of digits in fraction part. Assumes that any # thousands-separators have been removed, and that the # fraction part is delimited by {frsep}, which must be # either empty or a single character. If {xv} # has no {frsep}, or {frsep} is empty, returns -1. if (frsep == "") { return -1; } if (length(frsep) != 1) { prog_error(("ook!")); } # Strip any trailing blanks: gsub(/[ ]+$/, "", xv); # Locate {frsep} and count chars after it: k = index(xv, frsep); if (k <= 0) { return -1; } else { return length(xv) - k; } } function arg_error(msg) { printf "%s\n", msg > "/dev/stderr"; printf "usage: %s\n", usage > "/dev/stderr"; abort = 1; exit 1; } function data_error(msg) { printf "line %d: %s\n", FNR, msg > "/dev/stderr"; abort = 1; exit 1; } function prog_error(msg) { printf "line %d: %s\n", FNR, msg > "/dev/stderr"; abort = 1; exit 1; }