I'm a beginner to programming and to Progress, first post on StackOverflow, hope I'm posting in the right place!
I have a fillin field where I enter a time (hh:mm), character format. I also have two arrows, one pointing forward and one backward, and I want them to add / subtract 20 min respectively when pushed.
What would be a good way to write code for this? Turn the current time value to integer and seconds past midnight and then add /subtract 1200 sec? How would I get the result back to a hh:mm format to display in the fillin?
Any help greatly appreciated!
/Ellen
I think this will do what you need. Have the ON CHOOSE triggers on your arrow buttons run the changeMins procedure. Pass in the character time string from your fill-in and either "Add" or "Subtract". The output value will be the new adjusted time string. You can then set the screen value of your fill-in to that output value.
DEFINE VARIABLE cTime AS CHARACTER NO-UNDO.
cTime = "12:45".
RUN changeMins (INPUT-OUTPUT cTime, INPUT "Add").
MESSAGE cTime VIEW-AS ALERT-BOX INFORMATION BUTTONS OK.
PROCEDURE changeMins:
DEFINE INPUT-OUTPUT PARAMETER pcTime AS CHARACTER NO-UNDO.
DEFINE INPUT PARAMETER pcAction AS CHARACTER NO-UNDO.
DEFINE VARIABLE iHr AS INTEGER NO-UNDO.
DEFINE VARIABLE iMn AS INTEGER NO-UNDO.
/* Split the time string into hours and minutes */
ASSIGN
iHr = INTEGER(ENTRY(1, pcTime, ":"))
iMn = INTEGER(ENTRY(2, pcTime, ":"))
NO-ERROR.
IF ERROR-STATUS:ERROR THEN RETURN.
/* Adjust the time */
CASE pcAction:
WHEN "Add" THEN iMn = iMn + 20.
WHEN "Subtract" THEN iMn = iMn - 20.
END CASE.
/* Correct for boundaries */
IF iMn > 59 THEN
ASSIGN
iMn = iMn - 60
iHr = iHr + 1.
IF iMn < 0 THEN
ASSIGN
iMn = iMn + 60
iHr = iHr - 1.
IF iHr > 23 THEN iHr = iHr - 24.
IF iHr < 0 THEN iHr = iHr + 24.
/* Build the new time string */
pcTime = STRING(iHr, "99") + ":" + STRING(iMn, "99").
END PROCEDURE.
In this example, change the cTime string to different times and run it.
The Progress TIME function returns the integer number of seconds past midnight. So your idea of converting to such an integer is consistent with other uses within the 4gl which is a positive.
Personally I would keep the UI and the internal storage independent. So I would probably have a variable for the hour, another for the minute and a 3rd for seconds (if you need that). And I would use integers, rather than characters, for all 3.
I've no idea what version of Progress or what the environment that you are running in is but this quick and dirty little snippet might have some useful tidbits:
define variable hh as integer no-undo format ">9".
define variable mm as integer no-undo format "99".
define variable ss as integer no-undo format "99".
define variable myTime as integer no-undo.
form
hh mm ss
with
frame a
.
on value-changed of hh in frame a do:
if integer( self:screen-value ) > 23 then
do:
hh = 23.
display hh with frame a.
end.
end.
on value-changed of mm in frame a do:
if integer( self:screen-value ) > 59 then
do:
mm = 59.
display mm with frame a.
end.
end.
on value-changed of ss in frame a do:
if integer( self:screen-value ) > 59 then
do:
ss = 59.
display ss with frame a.
end.
end.
update hh mm ss with frame a.
myTime = (( hh * 3600 ) + ( mm * 60 ) + ss ).
display string( myTime, "hh:mm:ss am" ).
Deriving from TheDrooper's Code I would probably write something like the following code to fully utilize the built-in functions.
Shorter code that is still at least as easy to understand is often preferable.
Also consider that Progress does not have an optimizing compiler. If you can replace replace several simple statements with less statements (even if those are more complex and powerful than needed) the code is not only more maintainable but also faster.
DEFINE VARIABLE cTime AS CHARACTER NO-UNDO.
cTime = "12:45".
RUN changeMins (INPUT-OUTPUT cTime, INPUT "Add").
MESSAGE cTime VIEW-AS ALERT-BOX INFORMATION BUTTONS OK.
PROCEDURE changeMins:
DEFINE INPUT-OUTPUT PARAMETER pcTime AS CHARACTER NO-UNDO.
DEFINE INPUT PARAMETER pcAction AS CHARACTER NO-UNDO.
DEFINE VARIABLE iMn AS INTEGER NO-UNDO.
iMn = INTEGER(ENTRY(1, pcTime, ":")) * 60
+ INTEGER(ENTRY(2, pcTime, ":"))
NO-ERROR. /* Calculate minutes since midnight */
IF ERROR-STATUS:ERROR THEN RETURN.
/* Adjust the time */
CASE pcAction:
WHEN "Add" THEN iMn = iMn + 20.
WHEN "Subtract" THEN iMn = iMn - 20.
END CASE.
/* Build the new time string */
pcTime = string(iMn * 60, 'HH:MM'). /* Convert minutes to seconds and convert the result to a string */
END PROCEDURE.
If you need to allow for higher hour values (eg. if the field doesn't represent a time of day but a time interval) you can't just convert the resulting integer with the string function. In that case you could write
pcTime = string(iMn / 60, '99') + ':' + string(iMn mod 60, '99').
(Both TheDrooper and Tom Bascom seem to assume time of day.)
Related
To enter the correct mob number and dob we have to check the code in progres
Use LENGTH(STRING(mobnum)). This will convert it to a character string and give you the length of it. You can check to see if it is 10 characters long.
Making minimal changes to your approach, something along these lines should work:
define var mobnum as INT64 label "Mobile No:" format "9999999999" .
update mobnum.
if mobnum >= 1000000000 and mobnum < 10000000000 then
do:
display "Correct number".
end.
else
do:
display "enter exactly 10 digits please".
update mobnum.
end.
You can use LENGTH(mobnum) to get the quantity of digits into the number... like:
This make possible to create separate message for each of the lengths of mobnum.
DEF VAR mobnum AS INT NO-UNDO FORMAT "9999999999" INITIAL "99999999".
DEF VAR ii AS INT NO-UNDO.
ii = LENGTH(mobnum).
CASE ii:
WHEN 9 THEN DO:
DISPLAY "Number OK".
END.
OTHERWISE DO:
DISPLAY "Number is not 9-digit long".
END.
END CASE.
But if you want to make it simple, just use:
DEF VAR mobnum AS INT NO-UNDO FORMAT "9999999999" INITIAL "99999999".
DEF VAR ii AS INT NO-UNDO.
ii = LENGTH(mobnum).
IF ii = 9 THEN DO:
DISPLAY "Number correct".
END. ELSE DO:
DISPLAY "Number incorrect".
END.
What is the easiest way to achieve the "IN" SQL functionality in Progress? Here is my code block, and I want to limit this query to 5 different pin numbers for my test. I'd rather not use a string of "OR"s if I can avoid that.
//using the "IN" way with specific pin numbers
FOR EACH names NO-LOCK WHERE names.m-date GE 1/1/1900: //AND names.pin IN (179,198,200,201,210)
FOR EACH nacminfo NO-LOCK WHERE nacminfo.pin = names.pin:
FIND FIRST nacmtype WHERE nacmtype.contact_type_num EQ nacminfo.contact_type_num
AND nacmtype.descr MATCHES ("~*" + "email") NO-LOCK NO-ERROR.
IF AVAILABLE nacmtype THEN DO:
DISPLAY
nacmtype.type_val
nacmtype.descr.
END.
END.
END.
As Stefan says, you can use LOOKUP but performance may suffer since you will need to compare every record with the list.
Using a series of OR comparisons can be very efficient and if the list is short and static (like your example) not at all hard to do.
If the list is longer or changes frequently or if it is held in a variable then you might consider iterating over the list outside the FOR EACH.
Something like this:
define variable i as integer no-undo.
define variable j as integer no-undo.
define variable n as integer no-undo.
define variable myList as character no-undo.
myList = "179,198,200,201,210".
n = num-entries( myList ).
do j = 1 to n:
FOR EACH names NO-LOCK WHERE names.m-date GE 1/1/1900 AND names.pin = entry( j, myList ):
FOR EACH nacminfo NO-LOCK WHERE nacminfo.pin = names.pin:
FIND FIRST nacmtype NO-LOCK
WHERE nacmtype.contact_type_num EQ nacminfo.contact_type_num
AND nacmtype.descr MATCHES ("~*" + "email") NO-ERROR.
IF AVAILABLE nacmtype THEN DO:
DISPLAY
nacmtype.type_val
nacmtype.descr.
END.
END.
END.
end.
Or, finally, transform the list into a temp-table. Something like this:
define temp-table tt_myList no-undo
field namePIN as character
index namePIN-idx is unique primary namePIN.
.
define variable i as integer no-undo.
define variable n as integer no-undo.
define variable myList as character no-undo.
myList = "179,198,200,201,210".
/* build a TT */
n = num-entries( myList ).
do i = 1 to n:
create tt_myList.
tt_myList.namePIN = entry( i, myList ).
end.
for each tt_myList:
FOR EACH names NO-LOCK WHERE names.m-date GE 1/1/1900 AND names.pin = tt_myList.repName:
FOR EACH nacminfo NO-LOCK WHERE nacminfo.pin = names.pin:
FIND FIRST nacmtype NO-LOCK
WHERE nacmtype.contact_type_num EQ nacminfo.contact_type_num
AND nacmtype.descr MATCHES ("~*" + "email") NO-ERROR.
IF AVAILABLE nacmtype THEN DO:
DISPLAY
nacmtype.type_val
nacmtype.descr.
END.
END.
END.
end.
You could join the TT in the FOR EACH but it won't really make any difference and, personally, I find the nested FOR EACH syntax more natural.
Do you really need that FIRST? Can there ever be more than one record in the result of that FIND?
Lastly, MATCHES isn't doing you any performance favors. Hopefully the other parts of the WHERE clause are narrowing the result set enough that its impact is minimal.
Beware of the performance, since a function on the left side generally cannot use an index, but you can use the lookup function:
for each names
where names.m-date ge 1/1/1990
and lookup( string( names.pin ), '179,198,200,201,210' ) ) > 0
no-lock:
// do something
end.
I'd rather not use a string of "OR"s if I can avoid that.
As Stefan notes, using a function in the WHERE clause means that you will not use any indexes. That will impact performance, possibly very badly.
Look into using dynamic queries to build a WHERE clause with a a bunch of OR names.pin = 179 phrases.
You will need to tweak the building of the where string to make sure that it uses the best indexes available (and that's a huge topic by itself). You can see what indexes are used via the LOG-MANAGER - see https://docs.progress.com/bundle/openedge-abl-troubleshoot-applications-122/page/Query-information-logged.html for some info on this.
define variable pins as integet extent 5 initial [179,198,200,201,210] no-undo.
define variable loop as integer no-undo.
define variable cnt as integer no-undo.
define variable whereString as character no-undo.
define query q1 for names.
whereString = ' for each names no-lock where names.m-date GE 1/1/1900'.
cnt = extent(pins).
do loop = 1 to cnt:
whereSTring = whereSTring + substitute(' OR names.pin = &1', pins[loop]).
end.
query q1:query-prepare(whereString).
query q1:query-open().
query q1:get-first().
do while available names:
// do something with the names
query q1:get-next().
end.
finally:
query q1:query-close().
end finally.
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.
I have written a query with the help of one experienced person from progress 4GL but i missed one concept to ask. I have shared the query. Here what i need is to calculate the total orders based on start and end hour for every shift sequence (Total 21 seq) and assign to variable. For example look below
DEFINE VARIABLE StartHour06 AS INTEGER NO-UNDO.
DEFINE VARIABLE StartHour07 AS INTEGER NO-UNDO
FIND FIRST gdmf_shift WHERE gdmf_shift.shft_sequence = 1 NO-LOCK NO-ERROR.
StartHour06 = gdmf_shift.shft_start_hour.
StopHour07 = gdmf_shift.shft_stop_hour.
Like this i need to write the query up to 21 shift sequence..actually its not a good code..i need to make it simple and tried one method but from that i don't know how to do assign to variables. Please look below what i tried
DEFINE VARIABLE sSeq AS INTEGER EXTENT 21 NO-UNDO. /* start hour */
DEFINE VARIABLE eSeq AS INTEGER EXTENT 21 NO-UNDO. /* end hour */
FOR EACH gdmf_shift WHERE gdmf_shift.shft_sequence LE 21 NO-LOCK BY gdmf_shift.shft_sequence:
sSeq[1] = gdmf_shift.shft_start_hour.
eSeq[21] = gdmf_shift.shft_stop_hour.
DISP sSeq[1] eSeq[21].
END.
If you want to keep track of up to 21 individual shift sequences. Perhaps you should do something like the code below.
Right now you just update sSeq[1] and eSeq[21] for each iteration of the loop.
Insert shift sequence as index of the array instad:
DEFINE VARIABLE sSeq AS INTEGER EXTENT 21 NO-UNDO. /* start hour */
DEFINE VARIABLE eSeq AS INTEGER EXTENT 21 NO-UNDO. /* end hour */
FOR EACH gdmf_shift WHERE gdmf_shift.shft_sequence LE 21 NO-LOCK BY gdmf_shift.shft_sequence:
sSeq[gdmf_shift.shft_sequence] = gdmf_shift.shft_start_hour.
eSeq[gdmf_shift.shft_sequence] = gdmf_shift.shft_stop_hour.
DISP sSeq[gdmf_shift.shft_sequence] eSeq[gdmf_shift.shft_sequence].
END.
If you absolutely need individual variables instead on extents its would be a lot more code and a CASE-statement (could be an IF as well) deciding what variable to assign:
DEFINE VARIABLE sSeq1 AS INTEGER NO-UNDO. /* start hour */
DEFINE VARIABLE eSeq1 AS INTEGER NO-UNDO. /* end hour */
DEFINE VARIABLE sSeq2 AS INTEGER NO-UNDO. /* start hour */
DEFINE VARIABLE eSeq2 AS INTEGER NO-UNDO. /* end hour */
/* More variables here... */
DEFINE VARIABLE sSeq21 AS INTEGER NO-UNDO. /* start hour */
DEFINE VARIABLE eSeq21 AS INTEGER NO-UNDO. /* end hour */
FOR EACH gdmf_shift WHERE gdmf_shift.shft_sequence LE 21 NO-LOCK BY gdmf_shift.shft_sequence:
CASE gdmf_shift.shft_sequence:
WHEN 1 THEN DO:
sSeq1 = gdmf_shift.shft_start_hour.
eSeq1 = gdmf_shift.shft_stop_hour.
END.
WHEN 2 THEN DO:
sSeq2 = gdmf_shift.shft_start_hour.
eSeq2 = gdmf_shift.shft_stop_hour.
END.
/* more code here */
WHEN 21 THEN DO:
sSeq21 = gdmf_shift.shft_start_hour.
eSeq21 = gdmf_shift.shft_stop_hour.
END.
END CASE.
END.
But that will be LOTS of code. Consider using something like a temp-table instead if you dont like the array. If you name the fields in the temp-table like the names in the table you can use BUFFER-COPY otherwise you can ASSIGN any field.
DEFINE TEMP-TABLE tt NO-UNDO
FIELD shft_sequence LIKE gdmf_shift.shft_sequence
FIELD shft_start_hour LIKE gdmf_shift.s.shft_start_hour
FIELD shft_stop_hour LIKE gdmf_shift.s.shft_stop_hour.
FOR EACH gdmf_shift WHERE gdmf_shift.shft_sequence LE 21 NO-LOCK BY gdmf_shift.shft_sequence:
CREATE tt.
BUFFER-COPY gdmf_shift TO tt.
END.
I have raw finance text files that I'm importing into Access 2010 and exporting in Excel format. These files contain several 14 character length fields which represent dollar values. I'm having issues converting these fields into currency because of the 14th character. The 14th character is a number represented by a bracket or letter. It also dictates whether the unique field is a positive or negative value.
Positive numbers 0 to 9 start with open bracket { being zero, A being one, B being two,...I being nine.
Negative numbers -0 to -9 (I know, -0 is a mathematical faux pas but stay with me. I don't know how else to explain it.) start with close bracket } being -0, J being -1,K being -2,...R being -9.
Example data (all belonging to the same field/column):
0000000003422{ converted is $342.20
0000000006245} converted is -$624.50
0000000000210N converted is -$21.05
0000000011468D converted is $1,146.84
Here's the query that I'm working with. Each time I execute it, the entire field is deleted though. I would prefer to stick to a SQL query if possible but I'm open to all methods of resolution.
SET FIELD_1 = Format(Left([FIELD_1],12) & "." & Mid([FIELD_1],13,1) & IIf(Right([FIELD_1],1)="{",0,IIf(Right([FIELD_1],1)="A",1,IIf(Right([FIELD_1],1)="B",2,IIf(Right([FIELD_1],1)="C",3,IIf(Right([FIELD_1],1)="D",4,IIf(Right([FIELD_1],1)="E",5,IIf(Right([FIELD_1],1)="F",6,IIf(Right([FIELD_1],1)="G",7,IIf(Right([FIELD_1],1)="H",8,IIf(Right([FIELD_1],1)="I",9,"")))))))))),"$##0.00"), IIf(Right([FIELD_1],1)="}",0,IIf(Right([FIELD_1],1)="J",1,IIf(Right([FIELD_1],1)="K",2,IIf(Right([FIELD_1],1)="L",3,IIf(Right([FIELD_1],1)="M",4,IIf(Right([FIELD_1],1)="N",5,IIf(Right([FIELD_1],1)="O",6,IIf(Right([FIELD_1],1)="P",7,IIf(Right([FIELD_1],1)="Q",8,IIf(Right([FIELD_1],1)="R",9,"")))))))))),"-$##0.00")
here is a function that you can call to convert an input string like the ones in your example into a string formatted as you desire.
Private Function ConvertCurrency(strCur As String) As String
Const DIGITS = "{ABCDEFGHI}JKLMNOPQR"
Dim strAlphaDgt As String
Dim intDgt As Integer, intSign As Integer
Dim f As Integer
Dim curConverted As Currency
strAlphaDgt = Right(strCur, 1) ' Extract 1st char from right
f = InStr(DIGITS, strAlphaDgt) ' Search char in DIGITS. Its position is related to digit value
intDgt = (f - 1) Mod 10 ' Converts position into value of the digit
intSign = 1 - 2 * Int((f - 1) / 10) ' If it's in the 1st half is positive, if in the 2nd half of DIGITS it's negative
curConverted = intSign * _
CCur(Left(strCur, Len(strCur) - 1) & _
Chr(intDgt + 48)) / 100 ' Rebuild a currency value with 2 decimal digits
ConvertCurrency = Format(curConverted, _
"$#,###.00") ' Format output
End Function
If you need to have a Currency as returned value, you can change the type returned from String to Currency and return the content of curConverted variable.
Bye.