progress 4gl OpenEdge parse key/value-pair string aka Query-String - query-string

I cant find anything on how i should parse a string of key/value paris AKA query-string like this one:
FieldType="String"&FieldFormat="^[a-z0-9!#$%&'*+/=?^_`{|}~-]+$"
field separators might be contained in the value like the above example
this is not to be used as a web-request paramlist.
i found this: running a loop on a comma delimited list of items progress 4GL
but entry() does not care if the data is in a qoutation.
= EDIT =
So i found a not so ideal solution that i hope nobody needs to mimic
DO jj=1 TO NUM-ENTRIES(curr,"&"):
DEFINE VARIABLE pos AS INTEGER NO-UNDO.
ASSIGN
k = entry( 1, ENTRY(jj,curr,"&"), "=")
v = entry( 2, ENTRY(jj,curr,"&"), "=")
pos = INDEX( curr, k + "=" ).
/* Check if this is a qouted value*/
IF NUM-ENTRIES( SUBSTRING( curr, pos, ABS( INDEX(curr, "&", pos) - pos) ) ,'"') > 1 THEN
ASSIGN v = ENTRY( 2, SUBSTRING( curr, pos) , '"').
end.
The IF-statment is what nightmares are made of!

Building from Tom's and TheMadDBA's answers.
Assumption: The first & will be the one we want to split on.
define variable cQryString as character no-undo.
define variable iSplitIndex as integer no-undo.
define variable cType as character no-undo format "x(30)" label " Type".
define variable cFormat as character no-undo format "x(30)" label "Format".
assign
cQryString = 'FieldType=String&FieldFormat="^[a-z0-9!#$%&~'*+/=?^_`~{|}~~-]+$"'
iSplitIndex = index(cQryString, "&")
cType = substring(cQryString, 1, iSplitIndex - 1)
cFormat = substring(cQryString, iSplitIndex + 1, length(cQryString))
cType = substring(cType, index(cType, "=") + 1, length(cType))
cFormat = substring(cFormat, index(cFormat, "=") + 1, length(cFormat))
.
assign cType = entry(2, cType, '"') when substring(cType, 1, 1) = '"'.
assign cFormat = entry(2, cFormat, '"') when substring(cFormat, 1, 1) = '"'.
display
cType skip
cFormat
with side-labels.

define variable qryString as character no-undo.
define variable sep1 as character no-undo.
define variable sep2 as character no-undo.
define variable trimlist as character no-undo.
define variable sep1pos as integer no-undo.
define variable sep2pos as integer no-undo.
define variable part1 as character no-undo format "x(60)".
define variable part2 as character no-undo format "x(60)".
define variable name1 as character no-undo format "x(60)".
define variable name2 as character no-undo format "x(60)".
define variable valu1 as character no-undo format "x(60)".
define variable valu2 as character no-undo format "x(60)".
qryString = 'FieldType="String"&FieldFormat="^[a-z0-9!#$%&~'*+/=?^_`~{|}~~-]+$"'.
sep1 = '&'.
sep2 = '='.
trimlist = '"' + "'".
sep1pos = index( qryString, sep1 ).
part1 = substring( qryString, 1, sep1pos - 1 ).
part2 = substring( qryString, sep1pos + 1 ).
sep2pos = index( part1, sep2 ).
name1 = trim( substring( part1, 1, sep2pos - 1 ), trimlist ).
valu1 = trim( substring( part1, sep2pos + 1 ), trimlist ).
sep2pos = index( part2, sep2 ).
name2 = trim( substring( part2, 1, sep2pos - 1 ), trimlist ).
valu2 = trim( substring( part2, sep2pos + 1 ), trimlist ).
display
part1 skip
part2 skip
name1 skip
valu1 skip
name2 skip
valu2 skip
with
side-labels
.
(I have escaped special characters with a "~" inside the quoted string in order to include it in the program rather than get it from whatever input source you have. In real life qryString probably isn't embedded in the program.)

If you know the list of keys you can use the INDEX function to find the starting positions for all of key names and use SUBSTRING to pick apart the string.
display INDEX(<yourvar>,"&FieldFormat").
You could also use the extra option with NUM-ENTRIES and ENTRY to supply a delimiter to use ('"') as long as that will not show up inside of other quotes.

I think unless you want to do a brute force dissection, the only progress function which takes delimiters and quotes into account is IMPORT.
I think you'd have to mess about with streams, but have a look at this:
http://knowledgebase.progress.com/articles/Article/P112126
which discusses importing CSVs with commas in the fields.
I've tried using it, and while this test had issues with stream time-out, it kind of works:
def var test1 as char initial 'FldTp="String"&FldFmt="^[az0-9!#$%&+-/=^]+$"'.
def var kvp as char extent 10 format "x(50)".
def stream test.
input-output stream test through 'cat -u' unbuffered.
put stream test test1 format "X(100)" skip(1).
import stream test delimiter "=" kvp .
input-output stream test close.
display kvp with 1 column.
The interesting thing is that to get the "desired" result, you need to break on the "=" not the "&" since the quotes follow the "=". However, if you had some quoted and some non-quoted values this wouldn't work.
The output form this was:
┌────────────────────────────────────────────────────────────────────┐
│ kvp[1]: FldTp │
│ kvp[2]: String │
│ kvp[3]: &FldFmt │
│ kvp[4]: ^[a-z0-9!#$%&+-/=^]+$ │
│ kvp[5]: │
│ kvp[6]: │
│ kvp[7]: │
│ kvp[8]: │
│ kvp[9]: │
│ kvp[10]: │
└────────────────────────────────────────────────────────────────────┘
so it breaks the values correctly and you just need to remove the "&" from field +

Related

Using num-entries() with more than one possible delimitor

I have a list of email addresses that I am reading in from a csv file, that are sometimes separated by commas and sometimes by semicolons when the person has more than 1 email address.
Examples:
Pin email_address
11 heidi#gmail.com,hh#yahoo.com
12 tom#osu.edu;TQ#gmail.com
13 lisa#yahoo.com
14 linda#me.com;llewis#gmail.com,lvlv#yahoo.com
Let's say I am reading these into a variable and counting them per person with another variable:
DEFINE VARIABLE emailString AS CHARACTER NO-UNDO.
DEFINE VARIABLE iEmailCount AS INTEGER.
I want to count the number of emails for the person. I know that I can use this syntax to count the entries between semi-colons, and between commas respectively.
iEmailCount = NUM-ENTRIES (emailString, ";").
iEmailCount = NUM-ENTRIES (emailString).
But how can I best say...
iEmailCount = Num_ENTRIES(emailString,";" OR ",").
You have to break the line down by each delimiter. Loop through one delimiter, then the second:
DEFINE VARIABLE emailString AS CHARACTER NO-UNDO.
DEFINE VARIABLE iEmailCount AS INTEGER NO-UNDO.
DEFINE VARIABLE iLoop AS INTEGER NO-UNDO.
iEmailCount = NUM-ENTRIES(emailString).
DO iLoop = 1 TO NUM-ENTRIES(emailString): /* Loop through comma delimiters */
iEmailCount = iEmailCount + NUM-ENTRIES(ENTRY(iLoop, emailString), ";") - 1. /* Sum by semicolon delimiter */
END.
If you're sure that a semicolon will only appear as a delimiter in the data, you can replace them with commas. Then it's a simple count of the number of comma entries:
DEFINE VARIABLE emailString AS CHARACTER NO-UNDO INITIAL "linda#me.com;llewis#gmail.com,lvlv#yahoo.com".
DEFINE VARIABLE iEmailCount AS INTEGER NO-UNDO.
emailString = REPLACE(emailString, ";", ",").
iEmailCount = NUM-ENTRIES(emailString).
MESSAGE "Email string: " emailString SKIP
"Count: " iEmailCount VIEW-AS ALERT-BOX.
You can't in a single statement.
DEFINE VARIABLE emailString AS CHARACTER NO-UNDO.
DEFINE VARIABLE iEmailCount AS INTEGER.
iEmailCount = NUM-ENTRIES (emailString, ";") + NUM-ENTRIES (emailString).
For your pin 14 you will need to loop through the string, and count the entries manually.
define variable pin as character.
define variable pos as integer.
define variable pos2 as integer.
define variable cnt as integer.
define variable addr as character.
define variable startAt as integer.
pin = 'linda#me.com;llewis#gmail.com,lvlv#yahoo.com'.
startAt = 1.
pos = index(pin, ',', startAt).
do while pos gt 0:
addr = substring(pin, startAt, pos - 1).
pos2 = index(addr, ';').
if pos2 gt pos then
cnt = cnt + 1.
else
cnt = cnt + num-entries(addr, ';').
startAt = pos + 1.
pos = index(pin, ',', startAt).
end.
// after the last ,
if not trim(substring(pin, StartAt)) eq '' then
cnt = cnt + 1.
message
pin skip
cnt
view-as alert-box.
Just a gotcha, a string with a space will return 1 . An empty string will return 0.
message
'empty: ' num-entries('') skip // 0
'space: ' num-entries(' ') // 1
view-as alert-box.

How to be sure that only one record is taken into account at a given stage of the query?

I wonder how exactly the "queries" for each and first work.
lets say I have some tables A, B, C and D.
How many records will I find if I use sth like:
first B,
each C,
first D:
<block>
end.
1*|C|*1? [assuming there will be C that has some relation with B, etc]
And then I have sth like this:
for each A,
first B,
/*-----------------------*/
each C,
first D:
<block>
end.
How can I make sure to have the same complexity after this change? (B has now relation with A and there will be some where statements) Will it be |A|*1*|C|*1? And If I want to have the same complexity as before is there anything I can add there?
Example of the problem
define temp-table ttA no-undo
field mainID as character format "x(20)"
field foreignKey as character format "x(20)"
index idx_mainID is unique primary mainID
.
define temp-table ttB no-undo
field mainID as character format "x(20)"
field foreignKey as character format "x(20)"
index idx_mainID is unique primary mainID
.
define temp-table ttC no-undo
field mainID as character format "x(20)"
field foreignKey as character format "x(20)"
index idx_mainID is unique primary mainID
.
define temp-table ttD no-undo
field mainID as character format "x(20)"
field foreignKey as character format "x(20)"
index idx_mainID is unique primary mainID
.
DEFINE VARIABLE viCounterA as integer no-undo.
DEFINE VARIABLE viCounterB as integer no-undo.
DEFINE VARIABLE viCounterC as integer no-undo.
DEFINE VARIABLE viCounterD as integer no-undo.
DEFINE VARIABLE viTotal as integer no-undo.
DEFINE VARIABLE viTotal2 as integer no-undo.
assign
viCounterA = 0
viCounterB = 0
viCounterC = 0
viCounterD = 0
viTotal = 0
viTotal2 = 0
.
DEFINE VARIABLE vcAID as character no-undo.
DO viCounterA = 1 TO 9:
vcAID = "A0" + STRING(viCounterA).
create ttA.
ttA.mainID = vcAID.
ttA.foreignKey = "".
DEFINE VARIABLE vcBID as character no-undo.
DO viCounterB = 1 TO 9:
vcBID = vcAID + "B0" + STRING(viCounterB).
create ttB.
ttB.mainID = vcBID.
ttB.foreignKey = vcAID.
DEFINE VARIABLE vcCID as character no-undo.
DO viCounterC = 1 TO 9:
vcCID = vcBID + "C0" + STRING(viCounterC).
create ttC.
ttC.mainID = vcCID.
ttC.foreignKey = vcBID.
DEFINE VARIABLE vcDID as character no-undo.
DO viCounterD = 1 TO 9:
vcDID = vcCID + "D0" + STRING(viCounterD).
create ttD.
ttD.mainID = vcDID.
ttD.foreignKey = vcCID.
END.
END.
END.
END.
/* DISPLAY V1 */
for first ttB,
each ttC where ttC.foreignKey = ttB.mainID,
first ttD where ttD.foreignKey = ttC.mainID:
viTotal = viTotal + 1.
display
viTotal
ttD.mainID
.
end.
/* DISPLAY V2 */
for each ttA where ttA.mainID = "A01" or ttA.mainID = "A03",
first ttB where ttB.foreignKey = ttA.mainID,
each ttC where ttC.foreignKey = ttB.mainID,
first ttD where ttD.foreignKey = ttC.mainID:
viTotal2 = viTotal2 + 1.
display
viTotal2
ttD.mainID
.
end.
display
viTotal
viTotal2
.
It will be the same as:
define variable i as integer no-undo.
define variable j as integer no-undo.
define variable k as integer no-undo.
for each A no-lock:
i = i + 1.
for first B no-lock:
j = j + 1.
for each C no-lock where C.something = B.something:
k = k + 1.
display i j k.
end.
end.
end.

Progress 4GL: How to generate different log file name when multiple processes using the same Timestamp?

I am using this code for generating log files with timestamp so that each time when the procedure called by one process it will generate unique log filename. But the concern is that when two processes calling the same procedure at a time then the log files got over written. I don't want this. I want to find when procedures called by multiple processes and need to generate different log file name.
DEFINE VARIABLE TimeStamp AS CHARACTER NO-UNDO.
DEFINE VARIABLE cPath AS CHARACTER NO-UNDO.
ASSIGN
TimeStamp = string(month(today),"99") + string(day(today),"99") +~
string(year(today),"9999") + substring(string(time,"hh:mm:ss"),1,2) +~
substring(string(time,"hh:mm:ss"),4,2) +~
substring(string(time,"hh:mm:ss"),7,2)
cPath = "cim_" + STRING(TimeStamp) + ".log".
PROCEDURE generatecimlogfile:
OUTPUT TO VALUE(cPath).
MESSAGE "Inside a file".
END PROCEDURE.
RUN generatecimlogfile. /*(This procedure called by multiple processes at a time).*/
Instead of building a timestamp down to the second, you can build it to the millisecond. The NOW function will give you a datetime to the millisecond. This code will return such a timestamp:
DEFINE VARIABLE cTimestamp AS CHARACTER NO-UNDO.
RUN createTimestamp (OUTPUT cTimestamp).
MESSAGE cTimestamp VIEW-AS ALERT-BOX INFORMATION.
PROCEDURE createTimestamp :
DEFINE OUTPUT PARAMETER pcTimeStr AS CHARACTER NO-UNDO.
DEFINE VARIABLE cNow AS CHARACTER NO-UNDO.
DEFINE VARIABLE dtDate AS DATE NO-UNDO.
DEFINE VARIABLE cDate AS CHARACTER NO-UNDO.
DEFINE VARIABLE cTime AS CHARACTER NO-UNDO.
cNow = STRING(NOW, "99/99/9999 HH:MM:SS.SSS").
cDate = ENTRY(1, cNow, " ").
ASSIGN dtDate = DATE(cDate) NO-ERROR.
cTime = ENTRY(2, cNow, " ").
cTime = REPLACE(cTime, ":", "").
cTime = REPLACE(cTime, ".", "").
cTime = REPLACE(cTime, ",", "").
ASSIGN pcTimeStr = STRING(YEAR(dtDate), "9999") + STRING(MONTH(dtDate), "99") + STRING(DAY(dtDate), "99") + cTime NO-ERROR.
IF pcTimeStr = ? THEN pcTimeStr = "00000000000000000".
END PROCEDURE.
If that's not good enough, you can guarantee a unique filename by adding a GUID to the timestamp:
DEFINE VARIABLE cTimestamp AS CHARACTER NO-UNDO.
DEFINE VARIABLE cGUID AS CHARACTER NO-UNDO.
cGUID = GUID(GENERATE-UUID).
cTimestamp = "20200802123456789" + cGUID.
MESSAGE cTimestamp VIEW-AS ALERT-BOX INFORMATION.

How to add html tables in progress 4gl?

I created a temp table in Progress4gl and I need to email the data from temp table using html syntax. Which means I need to link all the fields in temp table to html table and email.
The fields in temp table are:
Part_ID, CustomerPartID, customer
Please help me with this.
Well you could just output the HTML. Something like this:
define temp-table tt_test
field f1 as integer
field f2 as character
field f3 as date
.
create tt_test.
assign
f1 = 1
f2 = "abc"
f3 = today
.
create tt_test.
assign
f1 = 2
f2 = "xyz"
f3 = today + 30
.
output to value( "mytable.html" ).
put unformatted "<table>" skip.
for each tt_test:
put unformatted substitute( " <tr><td>&1</td><td>&2</td><td>&3</td></tr>", f1, f2, f3 ) skip.
end.
put unformatted "</table>" skip.
output close.
For creating an html table you can (ab)use the power of datasets combined with serialize-name:
/* Write some awesome ABL code here, or load an existing snippet! */
define temp-table ttparts serialize-name "tr"
field part_id as char serialize-name "td"
field customerPartID as char serialize-name "td"
field customer as char serialize-name "td"
.
define dataset ds serialize-name "table" for ttparts .
define buffer bupart for ttparts.
create bupart.
assign
bupart.part_id = "one"
bupart.customer = "A"
.
create bupart.
assign
bupart.part_id = "two"
bupart.customer = "B"
.
def var lcc as longchar no-undo.
dataset ds:write-xml( "longchar", lcc, true ).
message string( lcc ).
https://abldojo.services.progress.com:443/#/?shareId=5d1618c14b1a0f40c34b8bc8
An updated version which accepts any temp-table handle and will return a longchar containing the HTML table:
function createHtmlTable returns longchar (
i_ht as handle
):
def var lcc as longchar.
def var hds as handle.
def var hb as handle.
def var ic as int.
create dataset hds.
hds:serialize-name = "table". // not needed if stripped below
create buffer hb for table i_ht.
hb:serialize-name = "tr".
hds:add-buffer( hb ).
do ic = 1 to hb:num-fields:
hb:buffer-field( ic ):serialize-name = "td".
end.
hds:write-xml( "longchar", lcc, true ).
// remove xml declaration
lcc = substring( lcc, index( lcc, "<", 2 ) ).
entry( 1, lcc, ">" ) = "<table". // see comment above
return lcc.
finally:
delete object hds.
end finally.
end function.
define temp-table ttparts
field part_id as char
field customerPartID as char
field customer as char
.
define buffer bupart for ttparts.
create bupart.
assign
bupart.part_id = "one"
bupart.customer = "A"
.
create bupart.
assign
bupart.part_id = "two"
bupart.customer = "B"
.
message string( createHtmlTable( temp-table ttparts:handle ) ).
https://abldojo.services.progress.com/#/?shareId=5d1760d84b1a0f40c34b8bcd
There is no built-in method to do this. One way that I could think of is converting the temp table to json and then json to HTML. I believe there are several utilities to convert json to html table. You can convert temp table to json in ABL using WRITE-JSON method.
An example from Progress documentation:
DEFINE VARIABLE cTargetType AS CHARACTER NO-UNDO.
DEFINE VARIABLE cFile AS CHARACTER NO-UNDO.
DEFINE VARIABLE lFormatted AS LOGICAL NO-UNDO.
DEFINE VARIABLE lRetOK AS LOGICAL NO-UNDO.
DEFINE TEMP-TABLE ttCust NO-UNDO LIKE Customer.
/* Code to populate the temp-table */
ASSIGN
cTargetType = "file"
cFile = "ttCust.json"
lFormatted = TRUE.
lRetOK = TEMP-TABLE ttCust:WRITE-JSON(cTargetType, cFile, lFormatted).

Dynamic Query in OpenEdge

Good day:
Quick question: Can I perform a dynamic query in OpenEdge?
Example:
def temp-table tt-num1
field f1 as int
field f2 as int.
def temp-table tt-num2
field f1 as int
field f2 as int.
def temp-table tt-num3
field f1 as int
field f2 as int.
What I need is something that looks like this:
procedure repeat-query:
for each 'variable that contains table-name' no-lock.
disp f1 f2.
end.
end procedure.
or some other way that can solve my problem.
How do I proceed with this? I tried to check for dynamic query on the Internet but with no luck. Thanks in advance.
If you go directly to https://documentation.progress.com/#page/progdocindex%2Fopenedge.html you can find documentation around everything OpenEdge. For instance dynamic queries.
I don't understand exactly what you try to do but here's an example of a dynamic query.
DEFINE TEMP-TABLE tt-num1 NO-UNDO
FIELD f1 AS INTEGER
FIELD f2 AS INTEGER.
DEFINE TEMP-TABLE tt-num2 NO-UNDO
FIELD f1 AS INTEGER
FIELD f2 AS INTEGER.
DEFINE TEMP-TABLE tt-num3 NO-UNDO
FIELD f1 AS INTEGER
FIELD f2 AS INTEGER.
CREATE tt-num1.
ASSIGN
tt-num1.f1 = 1
tt-num1.f2 = 1.
CREATE tt-num1.
ASSIGN
tt-num1.f1 = 1
tt-num1.f2 = 2.
CREATE tt-num1.
ASSIGN
tt-num1.f1 = 2
tt-num1.f2 = 1.
CREATE tt-num1.
ASSIGN
tt-num1.f1 = 2
tt-num1.f2 = 2.
DEFINE VARIABLE hQuery AS HANDLE NO-UNDO.
DEFINE VARIABLE cBuffer AS CHARACTER NO-UNDO.
DEFINE VARIABLE cField AS CHARACTER NO-UNDO.
DEFINE VARIABLE iValue AS INTEGER NO-UNDO.
ASSIGN
cBuffer = "tt-num1"
cField = "f1"
iValue = 1.
CREATE QUERY hQuery.
hQuery:ADD-BUFFER(cBuffer).
hQuery:QUERY-PREPARE("for each " + cBuffer + " where " + cBuffer + "." + cField + " = " + STRING(iValue)).
hQuery:QUERY-OPEN().
queryLoop:
REPEAT:
hQuery:GET-NEXT().
IF hQuery:QUERY-OFF-END THEN LEAVE queryLoop.
DISPLAY hQuery:GET-BUFFER-HANDLE(1):BUFFER-FIELD(cField):BUFFER-VALUE.
END.
hQuery:QUERY-CLOSE().
DELETE OBJECT hQuery.
As Stefan Drissen mentions in a very valid comment: the loop can be more compact:
DO WHILE hQuery:GET-NEXT():
/* Code goes here */
END.

Resources