How to Increment a Number with a trailing Letter

Chris Kelleher

Administrator
Staff member
>Jake Kacher wrote:
>set a variable containing a list of allowed characters "0123456789ABCD..YZ0".

> Notice a "0" at the end - this is a trick - when you take next letter
> after the one you need the index will automatically get reset to 0.

> > dalin wrote:
> > i'm in the middle of writing a program which involving incrementing a number with a letter....

A few years ago I published an article in Progressions describing
various routines like this. I called Connie Campbell and she graciously
permitted to reprint it here - horay for Progressions editors!

THE JOY OF CODING

Everybody now is an analyst solving global problems
and thinking in terms of modules and systems.

On every organization's chart the coder is the last,
the most insignificant person - one more personnel
reduction and he is history...

Well, to my view the chart is upside down -
without a coder there is no product to sell.

Taking a military analogy there are two kinds of
programmers: strategists and tacticians.

Strategists are the architects, analysts, consultants -
people who can step back and observe the system as a
whole, feel the trends and visualize the concepts.

Tacticians are the coders, implementors, installers -
people who, given a task, make things done through
their ingenuity, resourcefulness and initiative.

It doesn't mean tacticians can not plan or strategists can
not execute - just a reflection of their natural tendencies.

So why "The joy of coding" ? - Mental exercise!
Imagine solving crosswords and logic puzzles (your
favorite amusement) all day long and get paid for it too!

Seriously - coding is fun!

With this long preamble let's look at:

CONVERTING STRINGS TO NUMBERS

Have you ever heard complains about Progress not having a
function to determine whether a string is a number?

True, they do not have ISNUM(a) function but they have
three TRIM functions with a clever twist to supply your own
list of characters (mask) to trim.

Let's create the mask:
<BLOCKQUOTE><font size="1" face="Arial, Verdana">code:</font><HR><pre>
def var n-mask as char init "0123456789" no-undo.
[/code]

The string can be composed of up to three parts:
prefix, number and suffix.

For example determining a prefix takes a single call:
<BLOCKQUOTE><font size="1" face="Arial, Verdana">code:</font><HR><pre>

right-trim("AB1C025", n-mask) = "AB1C"
right-trim("AB1C02 ", n-mask) = "AB1C02 "
right-trim("A 1C025", n-mask) = "A 1C"
right-trim("AB1C 25", n-mask) = "AB1C "
right-trim(" 1234", n-mask) = "" .
[/code]

An important feature (in the last example) is that
"any number of spaces = no spaces" giving us a
flexibility not to test for a substring on every
position but still letting us differentiate
(if needed) by examining result's length as
the length(right-trim(" 1234", n-mask)) = 3.

Obviously the number portion starts at
"the next after prefix position":
<BLOCKQUOTE><font size="1" face="Arial, Verdana">code:</font><HR><pre>
substring(co-num,
length(right-trim(co-num, n-mask)) + 1)
)
[/code]

This is kind of hard to remember so we'll put it
(and a prefix) into procedure parse-prefix.

<BLOCKQUOTE><font size="1" face="Arial, Verdana">code:</font><HR><pre>
PROCEDURE parse-prefix:
/* Parse the string into a prefix and a number. */

def input parameter p-string as char no-undo.
def output parameter p-prefix as char no-undo.
def output parameter p-ch-num as char no-undo.

assign
p-prefix = right-trim(p-string, n-mask)
p-ch-num = substring(p-string, length(p-prefix) + 1).
END PROCEDURE.
[/code]

Sample Code:

<BLOCKQUOTE><font size="1" face="Arial, Verdana">code:</font><HR><pre>
def var c as char.
def var c2 as char.
def var i as int.

c = "AB00123".
run parse-prefix(c, output c2, output i).
displ c c2 length(c2) i with frame f.
pause.
[/code]

Parsing the suffix is necessary to split such things as
addresses into their fields to obtain street number:
<BLOCKQUOTE><font size="1" face="Arial, Verdana">code:</font><HR><pre>
left-trim("123 Main St.", n-mask) = " Main St."

for each customer where
left-trim(customer.address1, " ") <>
left-trim(customer.address1, " 0123456789"):

PROCEDURE parse-suffix:
/* Parse the string into a suffix and a number. */

def input parameter p-string as char no-undo.
def output parameter p-suffix as char no-undo.
def output parameter p-ch-num as char no-undo.

assign
p-suffix = left-trim(p-string, n-mask)
p-ch-num = substring(p-string, 1, length(p-string) -
length(p-suffix)).
END PROCEDURE.
[/code]

Notice we do not deal with possible decimal
values as things can get complicated with
multiple "." in the string.

The purpose of the parse-* routines is to deal with
alphanumeric keys many clients prefer to use in
their order numbers (coming from their old system)
where the naming conventions reflected order origin,
like: "C000001" - customer order, "I000002" -
inventory demand, "R000003" - repairs, etc.
(See the char2num i-p for proper decimal conversion).

If we are to deal with alphanumeric keys we must
provide for ways to:
1. Find last key for the key prefix (see last-key.i);
2. Get the next key (see get-next-key i-p);
3. Expand the key (see expand-key);
4. Sort by numeric portion of the key (see chr2num3.p).

<BLOCKQUOTE><font size="1" face="Arial, Verdana">code:</font><HR><pre>
/* last-key.i - Find the last key used for a given prefix
Jake Kacher; 03/03/96
*/
/* Usage:
&table - database table to search like "bol"
&where - additional conditions like when
the key is a second part of the index:
"bol.cust-num = cur-cust-num and"
&field - field to search like "co-num"
&f-len - field length
*/

&IF DEFINED(f-len) = 0 &THEN
&SCOPED f-len 7
&ENDIF

find last {&table} no-lock
where {&where}
{&table}.{&field} >= {&prefix}
and {&table}.{&field} < {&prefix} + chr(asc("9") + 1)
and length({&table}.{&field}) = {&f-len} no-error.

/* end of last-key.i */

on leave of co.co-num in frame f1 do:
def var t-key as char no-undo.
run expand-key(
self:screen-value,
LENGTH(STRING("", self:FORMAT)),
output t-key).
self:screen-value = t-key.
end.

find last co.
update co.co-num.

PROCEDURE expand-key:
/* Expand a key to its full length inserting "0" if needed */
def input parameter p-key as char no-undo.
def input parameter p-len as int no-undo.
def output parameter p-new-key as char no-undo.

def var t-prefix as char no-undo.
def var t-number as char no-undo.
def var t-number-format as char no-undo.
def var i as int no-undo.

run parse-prefix(p-key, output t-prefix, output t-number).

/* Remove a single or multiple "0" as a valid prefix */
/* Leave bizarre cases like "0.0" in */
do i = 1 to length(t-prefix):
if index(" 0", substring(t-prefix, i, 1)) = 0 then
leave.
end.
if i > length(t-prefix) then
t-prefix = "".
assign
i = length(t-prefix) + 1
t-number-format = fill(if i = 1 then ">" else "9",
p-len - length(t-prefix))
p-new-key = t-prefix + if int(t-number) > 0 then
string(int(t-number), t-number-format) else "".

END PROCEDURE. /* expand-key */
PROCEDURE char2num:
/* Tests if the string can be converted into a number
using supplied format (optional).

Sample Usage:
run char2num(c, '9999999',
output p-key-num, output p-key-new).
if RETURN-VALUE > "" then ... (UNDO, RETRY)

p-key-new will return "????" if a number can not
be displayed using a given format (e.g. (999 + 1)
in ">>9" format):
if p-key-new begins "?" then ... (UNDO, RETRY)

At this point you have a converted number in
p-key-new but you can also make a crucial test
to PROVE that what they typed is the same as a
resulted formatted string p-key-new: After all
they could have typed "5 - 2" which gets "3"
(unless you are making a free-hand expression
calculator) or "4.1" which converts to "4"
using format ">>>>9".
if substr(p-key-old,2) <> p-key-new then
... (UNDO, RETRY)
*/

def input parameter p-key-old as char no-undo.
def input parameter p-format as char no-undo.
def output parameter p-key-dec as dec no-undo.
def output parameter p-key-new as char no-undo.

/* Decimal function works equally well for integer values
and it can take a longer string (up to 50?) */
p-key-dec = decimal(p-key-old) no-error.
if error-status:error then return "Not numeric".

if p-format <> "" then
/* returns "????" if can't fit in the p-format */
p-key-new = string(p-key-dec, p-format) no-error.
else
/* left-justified */
p-key-new = string(p-key-dec).

/* must provide a return else RETURN-VALUE is not reset. */
RETURN.
END PROCEDURE.
[/code]

The following procedure gives a twist to the "add 1"
routine returning the next alphanumeric key in a
user-defined list:
def var c-mask as char init
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" no-undo.

You can let the user update the list to add/remove
character (maybe for some religious reasons they do
not like letter "F") but keep it sorted.

<BLOCKQUOTE><font size="1" face="Arial, Verdana">code:</font><HR><pre>
PROCEDURE get-next-key:
/* Get next numeric/character key */

def input parameter p-key as char no-undo.
def output parameter p-key2 as char no-undo.

def var i as int no-undo.
def var j as int no-undo.
def var c as char no-undo.
def var c-mask2 as char no-undo.

/* A trick to simplify finding the next (first) char. */
assign
c = substr(c-mask, length(c-mask), 1)
c-mask2 = c-mask +
if substr(c-mask, 1, 1) <> c then
c else "".
/* test before adding */
/* "Z" - the real last character */
c = substr(c-mask2, length(c-mask2) - 1, 1).

if fill(c, length(p-key)) = p-key then
return "Key is filled".

if length(trim(p-key, c-mask2)) > 0 then
return "Key is bad".

p-key2 = p-key.
do i = length(p-key) to 1 by -1:
assign
c = substr(p-key, i, 1)
j = index(c-mask2, c) + 1
substr(p-key2, i, 1) = substr(c-mask2, j, 1).

/* if "0" then bump another letter */
if length(c-mask2) > j then leave.
end.
RETURN.
END PROCEDURE.

/* chr2num3.p - Demo to sort character fields by numeric value.
Jake Kacher; 03/03/96
*/
/* Our Task:
For statistical purposes display and count co.co-num
organized in numeric key order and then by prefix.
Disregard ineligible co-num (not used as a key).
*/

def var i as int label "Group Count" no-undo.
def var i-group as int label "Key Value" no-undo.
def var i2 as int no-undo.
def var co-len as int no-undo.
def var n-mask as char init "0123456789" no-undo.

def temp-table co no-undo
field co-num as char format "x(7)"
index pi-co-num co-num.

def var co-nums as char no-undo.
assign
co-nums = "C000201,C000011,BA00010, 55, 44"
co-nums = co-nums + ",SAME011,ABC1, 333,AB, 25,D 1 666".

do i = 1 to num-entries(co-nums):
create co.
co.co-num = entry(i, co-nums).
end.

form co.co-num i-group i with down v6frame frame f1.
&SCOPED i-group ~
int(substr(co.co-num, length(right-trim(co.co-num, n-mask)) + 1))

/* Only a right-justified key is a proper key: otherwise
"12345 " is equal to " 12345" which makes them duplicate.
We must determine the size of the field to test for last
position.
*/
run get-widget-len(co.co-num:handle in frame f1, output co-len).
i = 0.

/* Progress can not index by a function but it sure can sort */
for each co no-lock
where INDEX(n-mask, substr(co.co-num, co-len, 1)) > 0
/* Do not want surprises from other programmers */
and length(co.co-num) = co-len
break by {&i-group}
by co.co-num:

assign
i = i + 1
i2 = i2 + 1.

displ co-num with frame f1.
if last-of({&i-group}) then do:
display {&i-group} @ i-group i with frame f1.
i = 0.
end.
down 1 with frame f1.
end.

/* It is preferable to print totals outside the loop to
insure the printing of at least intermediate results
on Escape rather then keeping inside with "if last("
*/
underline i-group i with frame f1.
/* All screen values are characters so we
can print "Total:" where a number should be */
display "Total Groups:" @ i-group i2 @ i with frame f1.
pause.

PROCEDURE get-widget-len:
/* Determines widget length from its format.
*/
def input parameter wh as WIDGET-HANDLE no-undo.
def output parameter wh-len as INTEGER no-undo.

/* Make sure DATA-TYPE is queryable (buttons?) */
if CAN-QUERY(wh, "DATA-TYPE") then
CASE wh
biggrin.gif
ATA-TYPE:
WHEN "character" OR
WHEN "integer" OR
WHEN "decimal" OR
WHEN "date" THEN
wh-len = LENGTH(STRING("", wh:FORMAT)).

/* get the longest of the two parts */
WHEN "logical" then
wh-len = MAX(LENGTH(ENTRY(1, wh:FORMAT, "/")),
LENGTH(ENTRY(2, wh:FORMAT, "/"))).
END CASE.
RETURN.
END PROCEDURE.
/* end of chr2num3.p demo */
[/code]

So, have I convinced you coding was fun?
 
Top