Is it possible to provide estimate hrs or minutes for progress completion? - PROGRESS 4GL - openedge

I am using below query for progress completion which shows how much percentage is completed in record updating process. My question or doubt here is Is it possible to provide estimated hrs/minutes for this progress completion?
For example, consider a table has 1000 records and each record gets into some validations before updating and approx time is less than a second(milliseconds). So if a single record takes less than a millisecond to update after gets to validations then what will be estimated for 1000 records? How to calculate and convert into hh:mm:ss? Please help to write a sample query.
DEFINE VARIABLE I AS INTEGER NO-UNDO.
DEFINE VARIABLE iPercentage AS INTEGER NO-UNDO.
DEFINE VARIABLE iComp AS INTEGER NO-UNDO.
DEFINE VARIABLE iRec AS INTEGER NO-UNDO.
ASSIGN
I = 0
iComp = 0
iRec = 0
iPercentage = 0.
/* Calculating Total records*/
FOR EACH <table> NO-LOCK:
iRec = iRec + 1.
END.
/* Taking each records from the same table to update*/
FOR EACH <table> NO-LOCK:
I = I + 1.
IF I = 1 THEN DO:
/*do some more validations*/
iComp = iComp + I.
iPercentage = 100 * (iComp / iRec).
IF iPercentage = 100 THEN DO:
MESSAGE "Record Updation Is completed".
END.
ELSE DO:
I = 0
NEXT.
END.
END.
END.

This variant here relies on NUM-RESULTS as suggested by nwahmaet and uses a DATETIME-TZ Variable and the INTERVAL function to determine the runtime. Output is done only every 1000's iteration so that the display of the Progress does not cause more load than the processing.
&scoped Table Customer
SESSION:APPL-ALERT-BOXES = FALSE .
DEFINE VARIABLE I AS INTEGER NO-UNDO.
DEFINE VARIABLE iPercentage AS INTEGER NO-UNDO.
DEFINE VARIABLE iComp AS INTEGER NO-UNDO.
DEFINE VARIABLE iRec AS INTEGER NO-UNDO.
DEFINE VARIABLE dtStart AS DATETIME-TZ NO-UNDO.
DEFINE VARIABLE iMsecs AS INTEGER NO-UNDO.
ASSIGN
I = 0
iComp = 0
iRec = 0
iPercentage = 0.
DEFINE QUERY qT FOR {&table} .
OPEN QUERY qT PRESELECT EACH {&Table}.
iRec = QUERY qT:NUM-RESULTS. // if you use a FOR you get 0
dtStart = NOW .
/* Taking each records from the same table to update*/
FOR EACH {&table} NO-LOCK:
I = I + 1.
IF I MODULO 1000 = 0 THEN DO:
PAUSE .05 . // simulate some load
/*do some more validations*/
iComp = iComp + I.
iPercentage = 100 * (iComp / iRec).
iMsecs = INTERVAL (NOW, dtStart, "milliseconds") .
MESSAGE i "of" iRec "/" iMsecs "/"
STRING (INTEGER ((iMsecs / (i / iRec) // iMsecs / % processed = estimated total time
- iMsecs) // minus time already spent
/ 1000), // msecs to seocnds
"hh:mm:ss") // format output
"remaining".
END.
END.
MESSAGE "Record Updation Is completed".

It is not simple to do in ABL. Maybe not even possible. I think you would have to keep track - ie measure how long such a query takes, and store that somewhere.
You could have a db table that has a query name, num records returned (maybe?) and time taken. You can use those records to determine an average time of how long it take to find, say, 100 records and use that with the percentage progress bar. After each query, you create a record in that table recording how long it too.
But doing that is going to add time to the processing of that query. ANd there's nothing in the DB itself that does this for you.
There are also faster method of getting the query count than looping through them all twice.
You can use a query with a PRESELECT clause, which can give you the count.
define query qT for Table.
open query qT preselect each Table.
iRec = query qT:num-results. // if you use a FOR you get 0
get first qT no-lock.
do while available Table:
if query qT:current-result-row mod 50 = 0 then
. // update the progress bar
// do stuff with the data
get next qT no-lock.
end.

The bit that you are missing is the elapsed time so far. That's pretty easy to obtain:
DEFINE VARIABLE I AS INTEGER NO-UNDO.
DEFINE VARIABLE iPercentage AS INTEGER NO-UNDO.
DEFINE VARIABLE iComp AS INTEGER NO-UNDO.
DEFINE VARIABLE iRec AS INTEGER NO-UNDO.
ASSIGN
I = 0
iComp = 0
iRec = 0
iPercentage = 0.
/* Calculating Total records*/
FOR EACH <table> NO-LOCK:
iRec = iRec + 1.
END.
etime( true ). // set the etime counter to 0
/* Taking each records from the same table to update*/
FOR EACH <table> NO-LOCK:
I = I + 1.
IF I = 1 THEN DO:
/*do some more validations*/
iComp = iComp + I.
iPercentage = 100 * (iComp / iRec).
IF iPercentage = 100 THEN DO:
MESSAGE "Record Updation Is completed".
END.
ELSE DO:
message string( 1000 * ((( etime / ( iComp / iRec )) - etime )) , "hh:mm:ss" ) "remaining".
I = 0
NEXT.
END.
END.
END.

Related

How to group 24-hour data in 30 minutes interval to get the count?

I have a field name timestamp in the sales table in which the data format is: 20210725.1800 which means 2021-year 07-month 25th-date 00:30:00 AM.
Now, if I want to count the sales on between 30 minutes intervals from 20210725.0000 to 20210725.1800, I can do that by simply writing:
def var k as int no-undo.
for each sales no-lock
where salesdate = 07/25/2021
and timestamp >= 20210725.0000
and timestamp <= 20210725.1800
:
if available sales then do:
k = k + 1.
pause 0.
display k with frame f.
end.
end.
But, I don't want to run the same query 24 times by changing the start and end time of the timestamp field.
So, I am looking for a smarter way to find out the whole day sales count grouped by 30 minutes intervals on this timestamp field.
You can do something along the lines of the below. You could also sort the query by salesdate if you wanted more than one date too, though you'd have to clear the integer array on each new date (see the byandbreak by` doc on how to do that).
The calculation of the ts variable will depend on how the decimal value was constructed: are .1800 and .18002 in the same 30 minute slot?
def var numSales as integer extent 24 no-undo.
def var k as integer no-undo.
def var ts as integer.
for each sales
no-lock
where salesdate eq 07/25/2021:
// get the timestamp for the day
// this could use a calculation like the below,
// or a simple CASE statement
ts = 10000 * (timestamp - decimal(string(year(salesdate)) + string(month(salesdate)) + string(day(salesdate)))).
// find the 30-minute slot. +1 makes it a 'ceiling'
numSales[integer(ts / (30 * 60)) + 1] += 1.
end.
do k = 1 to 24 with frame f:
displ
k (k * 30 * 60) numsales[k].
end.
There really isn't anything wrong with running the same query 24 times (or whatever) so long as each sub query is as efficient as one big query.
It is especially not wrong to do so if it makes your code clearer to the maintenance programmer that comes along 3 years from now trying to understand what you did.
The following example just uses a DATE field since the ubiquitous "sports" database does not have any fields with the style of date.time that your example has, but it should be simple to extrapolate from:
define variable n as integer no-undo.
define variable d as date no-undo.
define variable b as handle no-undo.
define variable q as handle no-undo.
create buffer b for table "order".
create query q.
q:set-buffers( b ).
do d = 1/1/1998 to 1/31/1998:
n = 0.
q:query-prepare( substitute( 'preselect each order no-lock where orderDate = &1', d )).
q:query-open no-error.
if q:query-off-end = no then
n = q:num-results no-error.
display d n with frame a down.
down with frame a.
q:query-close no-error.
end.
If I step over my question about how your timestamp is really constructed, you can use break by to get your results:
def temp-table ttsales no-undo
field salesdate as date
field timestamp as decimal
.
function createSale returns logical (
i_detimestamp as decimal
):
def buffer busale for ttsales.
def var idate as int.
def var iyear as int.
def var imonth as int.
def var iday as int.
assign
idate = truncate( i_detimestamp, 0 )
iyear = truncate( idate / 10000, 0 )
idate = idate - iyear * 10000
imonth = truncate( idate / 100, 0 )
iday = idate - imonth * 100
.
create busale.
assign
busale.salesdate = date( imonth, iday, iyear )
busale.timestamp = i_detimestamp
.
end function.
createSale( 20210725.0000 ).
createSale( 20210725.0001 ).
createSale( 20210725.1799 ).
createSale( 20210725.1800 ).
createSale( 20210725.1801 ).
def buffer busale for ttsales.
def var irecords as int.
def var idate as int.
for each busale
where busale.salesdate = 07/25/2021
break
by truncate( busale.timestamp * 10000 / 1800, 0 )
:
irecords = irecords + 1.
if last-of( truncate( busale.timestamp * 10000 / 1800, 0 ) ) then do:
display
int( truncate( ( ( busale.timestamp * 10000 ) modulo 10000 ) / 1800, 0 ) )
irecords
.
irecords = 0.
end.
end.
Try it out in ABLdojo.

How to use Windows API 'GetTcpTable' to find a avaiable tcp port?

I'm trying to use windows api 'GetTcpTable', but I don't know how to prepare the data structure 'PMIB_TCPTABLE' and get the return value from it.
PROCEDURE GetTcpTable EXTERNAL "Iphlpapi":U:
DEFINE OUTPUT PARAMETER mTcpTable AS HANDLE TO MEMPTR. // the API expects a pointer, so use HANDLE TO syntax to pass one
DEFINE INPUT-OUTPUT PARAMETER SizePointer AS HANDLE TO LONG.
DEFINE INPUT PARAMETER Order AS LONG.
DEFINE RETURN PARAMETER IPHLPAPI_DLL_LINKAGE AS LONG.
END PROCEDURE.
DEFINE VARIABLE mTcpTable AS MEMPTR NO-UNDO.
DEFINE VARIABLE SizePointer AS INT NO-UNDO.
DEFINE VARIABLE Order AS INT NO-UNDO.
DEFINE VARIABLE IPHLPAPI AS INT NO-UNDO.
DEFINE VARIABLE mTempValue AS MEMPTR NO-UNDO.
DEFINE VARIABLE dwNumEntries AS INT NO-UNDO.
DEFINE VARIABLE dwState AS INT NO-UNDO.
DEFINE VARIABLE dwLocalAddr AS INT64 NO-UNDO.
DEFINE VARIABLE dwLocalPort AS INT NO-UNDO.
DEFINE VARIABLE dwRemoteAddr AS INT64 NO-UNDO.
DEFINE VARIABLE dwRemotePort AS INT NO-UNDO.
DEFINE VARIABLE ix AS INTEGER NO-UNDO.
SizePointer = 4.
Order = 1.
SET-SIZE(mTcpTable) = SizePointer.
RUN GetTcpTable(OUTPUT mTcpTable, INPUT-OUTPUT SizePointer,INPUT Order, OUTPUT IPHLPAPI). //get ERROR_INSUFFICIENT_BUFFER and know the real buffer now
MESSAGE "IPHLPAPI is " IPHLPAPI SKIP // ERROR_INSUFFICIENT_BUFFER = 122
"SizePointer is " SizePointer SKIP
VIEW-AS ALERT-BOX.
SET-SIZE(mTcpTable) = 0.
SET-SIZE(mTcpTable) = SizePointer.
RUN GetTcpTable(OUTPUT mTcpTable, INPUT-OUTPUT SizePointer,INPUT Order, OUTPUT IPHLPAPI).
MESSAGE "IPHLPAPI is " IPHLPAPI SKIP // NO_ERROR = 0
"SizePointer is " SizePointer SKIP
GET-LONG(mTcpTable,1) SKIP //dwNumEntries
VIEW-AS ALERT-BOX.
IF IPHLPAPI = 0 THEN DO:
dwNumEntries = GET-LONG(mTcpTable,1).
OUTPUT TO VALUE ("C:\temp\debug.txt") UNBUFFERED.
DO ix = 0 TO dwNumEntries - 1.
dwState = GET-UNSIGNED-LONG(mTcpTable,5 + ix * 20). // get value of dwState
dwLocalAddr = GET-UNSIGNED-LONG(mTcpTable,9 + ix * 20). // get value of dwLocalAddr
SET-SIZE(mTempValue) = 2.
PUT-BYTE(mTempValue,2) = GET-UNSIGNED-SHORT(mTcpTable,13 + ix * 20).
PUT-BYTE(mTempValue,1) = GET-UNSIGNED-SHORT(mTcpTable,14 + ix * 20).
dwLocalPort = GET-UNSIGNED-SHORT(mTempValue,1). // get value of dwLocalPort, The maximum size of an IP port number is 16 bits, so only the lower 16 bits should be used. The upper 16 bits may contain uninitialized data.
SET-SIZE(mTempValue) = 0.
dwRemoteAddr = GET-UNSIGNED-LONG(mTcpTable,17 + ix * 20). // get value of dwRemoteAddr
SET-SIZE(mTempValue) = 2.
PUT-BYTE(mTempValue,2) = GET-UNSIGNED-SHORT(mTcpTable,21 + ix * 20).
PUT-BYTE(mTempValue,1) = GET-UNSIGNED-SHORT(mTcpTable,22 + ix * 20).
dwRemotePort = GET-UNSIGNED-SHORT(mTempValue,1). // get value of dwRemotePort, The maximum size of an IP port number is 16 bits, so only the lower 16 bits should be used. The upper 16 bits may contain uninitialized data.
SET-SIZE(mTempValue) = 0.
PUT UNFORMATTED dwState "~t" dwLocalAddr "~t" dwLocalPort "~t" dwRemoteAddr "~t" dwRemotePort "~r".
END.
OUTPUT CLOSE.
END.
SET-SIZE(mTcpTable) = 0.
Is there any better way to get the lower 16 bits? I'm not sure why I need to put the byte in a reversed order in to mTempValue, is it releated to little endian of x86 PC?
How to get the right start position to read the value if I run the code on different bitness computer
I think the question can be closed now:
https://community.progress.com/s/feed/0D54Q000088eIeASAU?t=1602495359075&searchQuery
Thank you guys :)
Microsoft's doc usually has the definitions of the structures (see https://learn.microsoft.com/en-us/windows/win32/api/tcpmib/ns-tcpmib-mib_tcptable). Once you know those - and any other that are related/pointed to/etc - you can create and manipulate them using MEMPTR data types and the various functions that operate on them: SET-SIZE(), GET-BYTES() and GET-POINTER-VALUE() are typically used. There is doc on this at https://docs.progress.com/bundle/openedge-programmimg-interfaces/page/Shared-Library-and-DLL-Support.html .

Tracing cases of failure got

I'm trying to populate an integer variable from a character variable. If there is any error found I want to show the error message and trace all the possible cases for failure got.
//Defining variable
Define variable char_value as character no-undo initial "kk".
Define variable int_value as integer no-undo.
define variable ix as integer no-undo.
Assign int_value = integer(char_value) no-error.
IF ERROR-STATUS:ERROR OR ERROR-STATUS:NUM-MESSAGES > 0 THEN
DO:
MESSAGE ERROR-STATUS:NUM-MESSAGES
" errors occurred during conversion." SKIP
"Do you want to view them?"
VIEW-AS ALERT-BOX QUESTION BUTTONS YES-NO
UPDATE view-errs AS LOGICAL.
IF view-errs THEN
DO ix = 1 TO ERROR-STATUS:NUM-MESSAGES:
MESSAGE ERROR-
STATUS:GET-NUMBER(ix)
ERROR-STATUS:GET-
MESSAGE(ix).
END.
END.
There are two conditions which I want to know.
What char value I gave so that no. Of error returns will be more than 1.
How can I trace all the possible cases for failure got.
The built-in conversion routine does not do what you want it to do. So you will need to parse your input prior to attempting to convert it. Something like this:
function isDigit returns logical ( input d as character ):
if length( d ) = 1 then
return ( index( "0123456789", d ) > 0 ).
else
return no.
end.
procedure checkInteger:
define input parameter integerString as character no-undo.
define output parameter errorList as character no-undo.
define output parameter ok as logical no-undo.
define variable i as integer no-undo.
define variable n as integer no-undo.
define variable c as character no-undo.
ok = yes.
n = length( integerString ).
do i = 1 to n:
c = substring( integerString, i, 1 ).
if i = 1 and c = "-" then next.
if isDigit( c ) = no then
do:
ok = no.
errorList = errorList + substitute( "The character '&1' at offset &2 is not a valid integer value~n", c, i ).
end.
end.
errorList = trim( errorList, "~n" ). // remove the trailing newline (if any)
return.
end.
define variable ok as logical no-undo.
define variable errorList as character no-undo.
run checkInteger( "12x34y56z789", output errorList, output ok ).
if ok = yes then
message "string is a properly formed integer, go ahead and convert it".
else
message
"string was not correctly formed, do not try to convert it" skip
errorList
view-as alert-box information
.
Note #1 If the input contains unprintable characters the errorList string will display it literally and it will look kind of funny. You could, of course, encode them to be more readable. Doing so is left as an exercise. Or another question.
Note #2 This code makes no attempt to check that the string value will fit into an integer or an int64. That is also left as an exercise.
While you can make your parsing as complex as you like, I would just keep it simple and ensure the user is provided enough information, which in this case is the complete input value:
def var cc as char initial "kk".
def var ii as int.
ii = integer( cc ).
catch e as progress.lang.error:
message quoter( cc, "'" ) e:getMessage(1) view-as alert-box.
end catch.

How to parse two characters at a time for one Iteration?

I have a written query for matching 2 characters and parse the data but I feel that the way i did is wrong. Let me share my logic with you
DEFINE VARIABLE I AS INTEGER NO-UNDO.
DEFINE VARIABLE cData AS CHARACTER NO-UNDO.
DEFINE VARIABLE cParsData AS CHARACTER NO-UNDO.
ASSIGN
cData = 'PRRSCLPP0123456789'.
DO I = 1 TO LENGTH(cData):
cParsData = SUBSTRING(cData,I).
IF cParsData MATCHES 'PP*' THEN MESSAGE SUBSTRING(cParsData,4,9).
END.
As you see the way i did is wrong and its parsing each character per iteration i think but what i need is it should parse two characters per iteration so that we can matches "PP". You can share or change the logic for different ways to get the same output
It is hard to imagine a reason for iterating through the string one character at a time looking for "PP" and then spitting out characters 4 through 13. It would be much simpler to do this:
define variable myData as character no-undo.
define variable foundIt as integer no-undo.
myData = "PRRSCLPP0123456789".
foundIt = index( myData, "PP" ).
if foundIt > 0 then
message substring( myData, 4, 9 ).
If there is a reason to go through that string one character at a time I think it must not be contained in your code sample or question.
On a side note: MATCHES "PP*" is equivalent to BEGINS "PP". It doesn't matter much in this case but it is a bad habit to needlessly throw MATCHES at string comparisons. Especially if that habit ends up in a WHERE clause. Using MATCHES in WHERE clauses will cause a table scan. Which is almost always a bad idea.
If you are trying to output N characters after the position that "PP" was found (rather than the hard-coded 4 through 13) you would do it like so (assuming that n = 9):
define variable myData as character no-undo.
define variable foundIt as integer no-undo.
myData = "PRRSCLPP0123456789".
foundIt = index( myData, "PP" ).
if foundIt > 0 then
message substring( myData, foundIt + 1, 9 ).
I dont quite understand what you want to do. Do you want to search the string and see if there's "PP" in it? Then you don't need to do it in an iteration. Simply
cData MATCHES "*PP*" will tell you that.
If "PP" is some kind of delimiter and you want to do something with the data before and after you can do:
DEFINE VARIABLE I AS INTEGER NO-UNDO.
DEFINE VARIABLE cData AS CHARACTER NO-UNDO .
DEFINE VARIABLE cParsData AS CHARACTER NO-UNDO.
ASSIGN
cData = 'PRRSCLPP0123456789'.
DO I = 1 TO LENGTH(cData):
cParsData = SUBSTRING(cData,I, 2).
IF cParsData = 'PP' THEN DO:
DISPLAY
SUBSTRING(cData, i + 2) FORMAT "x(20)" LABEL "After PP"
SUBSTRING(cData, 1, i - 1) FORMAT "x(20)" LABEL "Before PP".
END.
END.
This only works for one occurance of "PP" in the string though. You should try to explain better exactly what you are after.
You left a lot more information in a comment on another answer:
If PP really always is position 10 (and 11) or 20 (and 21) and you only want the follwing 10 chars then you can do:
DEFINE VARIABLE cData1 AS CHARACTER NO-UNDO.
DEFINE VARIABLE cData2 AS CHARACTER NO-UNDO.
/* Position 10 and 11 */
cData1 = 'PRRSCLAAAPP0123456789'.
/* Position 20 and 21 */
cData2 = 'PRRSCLAAAPRRSCLAAAPP9876543210AA'.
FUNCTION parse RETURNS CHARACTER
(INPUT cString AS CHARACTER ):
IF INDEX(cString, "PP") > 0 THEN
RETURN SUBSTRING(cString, INDEX(cString, "PP") + 2, 10 ).
ELSE
RETURN "".
END.
MESSAGE cData1 " ->" parse(cData1) SKIP
cData2 " ->" parse(cData2) VIEW-AS ALERT-BOX.

How to convert A00073 value to 9973 in progress 4gl

i have column having multiple value like A0045 ,A00065 . i want to convert it 9945, 9965.
Need to remove all 0 and character value and add 99 before that value.. Please help..
This can be done in many ways. Here is one way (may not be the best). As I don't have a database, I created a temp table.
def temp-table tt
field val as char.
create tt.
tt.val = "A0045".
create tt.
tt.val = "A00065".
for each tt:
run filter_zero(input-output val).
val = replace(val,"A","99").
DISP val.
end.
procedure filter_zero:
define input-output parameter val as character.
// remvoe all zeroes
repeat while val matches("*0*"):
val = replace(val,"0","").
end.
end procedure.
The code below removes the uppercase (A-Z) and lowercase letter (a-z) and "0" as well :
DEF TEMP-TABLE test
FIELD str1 AS CHAR.
DEF VAR newStr AS CHAR NO-UNDO.
DEF VAR i AS INT NO-UNDO.
CREATE test.
ASSIGN test.str1 = "A0045".
CREATE test.
ASSIGN test.str1 = "A00065".
FOR EACH test:
DO i = 65 TO 90: /* A - Z */
IF SUBSTR(test.str1, 1, 1) EQ CHR(i) THEN
DO:
test.str1 = REPLACE(test.str1, CHR(i), "").
END.
END.
DO i = 97 TO 122: /* a - z */
IF SUBSTR(test.str1, 1, 1) EQ CHR(i) THEN
DO:
test.str1 = REPLACE(test.str1, CHR(i), "").
END.
END.
/* Removes all 0 and add 99 before the value */
test.str1 = REPLACE(test.str1, "0", "").
ASSIGN test.str1 = "99" + test.str1.
DISP test.
END.
define variable word as character no-undo.
define variable i as integer no-undo.
assign word = "A00065".
/*******To Remove all the zero********/
word = replace(word,substring(word,index(word,"0"),(r-index(word,"0") - 1)),"").
do i = 65 to 90:
if substring(word,1,1) = chr(i) then
do:
word = replace(word,substring(word,1,1),"99").
leave.
end.
end.
display word.
DEFINE VARIABLE word AS CHARACTER NO-UNDO.
SET word.
word = REPLACE(word,SUBSTRING(word,1,R-INDEX(word,"0")),"99").
/* If you want to remove all words before last zero
(including last zero also) * /
MESSAGE word
VIEW-AS ALERT-BOX INFORMATION BUTTONS OK.
Please do let me know if this works for you, Progress not installed so couldn't compile and test.
/* A0045 -> 9945
A00065 -> 9965 */
DEFINE VARIABLE v_RawData AS CHARACTER NO-UNDO.
DEFINE VARIABLE v_InpData AS CHARACTER NO-UNDO.
DEFINE VARIABLE i AS INTEGER NO-UNDO.
DEFINE VARIABLE j AS INTEGER NO-UNDO.
ASSIGN v_RawData = "A0045,A00065".
DO i =1 TO NUM-ENTRIES(v_RawData):
ASSIGN v_InpData = ENTRY(i,v_RawData).
DO j = 1 TO LENGTH(v_InpData):
IF ASC(SUBSTRING(v_InpData,j,1)) > 48 AND
ASC(SUBSTRING(v_InpData,j,1)) < 58 THEN
DO:
LEAVE.
END.
END.
MESSAGE REPLACE(v_InpData,SUBSTRING(v_InpData,1,j - 1),"99").
END.

Resources