Consider the following XML document that I receive and parse:
<Courses>
<Course>
<Name>Intro to DB</Name>
<Credits>3.0</Credits>
</Course>
<Course>
<Name>Intro to Programming</Name>
<Credits>3.0</Credits>
</Course>
</Courses>
If I want to add a new course between two courses or a new element within each course (e.g. courseId). How can I achieve this in Progress? I've given a read to Progress' XML documentation (for DOM and SAX) and found INSERT-BEFORE(), not sure if that can be used to achieve this.
Do I have to use temp-table/ ProDataSets to achieve this?
a example with a temp-table and a dataset
the dataset here is only defined for formatting the xml output
DEFINE TEMP-TABLE ttCourse NO-UNDO XML-NODE-NAME "Course"
FIELD dSORT AS DECIMAL SERIALIZE-HIDDEN // hidden in xml output
FIELD Name AS CHARACTER
FIELD Credits AS DECIMAL
FIELD NEWFIELD AS CHARACTER
INDEX SORT dSORT ASCENDING. // this index for sorting the xml output Course
DEFINE VARIABLE cxml AS LONGCHAR NO-UNDO.
DEFINE VARIABLE iCnt AS INTEGER NO-UNDO.
// DEFINE DATASET only for xml-node-name
DEFINE DATASET ds
XML-NODE-NAME "Courses" FOR ttCourse.
cxml = "<Courses>
<Course>
<Name>Intro to DB</Name>
<Credits>3.0</Credits>
</Course>
<Course>
<Name>Intro to Programming</Name>
<Credits>3.0</Credits>
</Course>
</Courses>".
TEMP-TABLE ttCourse:READ-XML ("LONGCHAR", cxml, "MERGE", "", FALSE, ?, ?).
// for sorting output use field dsort
FOR EACH ttCourse WHERE ttCourse.DSORT = 0:
iCnt = iCnt + 1.
ttCourse.DSORT = iCnt.
END.
// insert a record in the middle
CREATE ttCourse.
ASSIGN
ttCourse.DSORT = 1.5
ttCourse.name = "test"
ttCourse.credits = 5.5
ttCourse.NEWFIELD = "NEWFIELD"
.
DATASET ds:WRITE-XML ("file", "C:/temp/baz/courses.xml", TRUE,?,?,FALSE,FALSE).
Related
I am reading in a list of constituent attributes (tags) and only want to keep a subset of them. Here is how I attempted to do that.
DEFINE VARIABLE tagsToKeep AS CHARACTER.
tagsToKeep = "Business Contact,Celebrate 2015,Celebrate 2017,Celebrate 2019,Certificate - Former Holder,Non-MB Church".
/*do a bunch of stuff to get the cRecord which is one tag, often with spaces or other characters in it.*/
IF INDEX(tagsToKeep, cRecord) > 0 THEN
DO:
CREATE ttTag.
ASSIGN
ttTag.tag-code = cRecord
ttTag.constituent-id = ttAccount.pin-string.
ttTag.tag-name = cRecord.
END.
The trouble is that one of the tags is "Certificate" which is a substring of one of the strings in the TagsToKeep string -- "Certificate - Former Holder" gets included and created as a Tag. I only want to match on the strings that are essentially "whole word only".
Is there a (better) way to do what I want to do?
In your code, use LOOKUP instead of INDEX
IF LOOKUP (cRecord, tagsToKeep) > 0 THEN
(comma-) delimited lists are a key concept in the ABL.
If cRecord is a single tag, then it may be useful to take the list of to-keep tags and put those tags into a separate temp-table
DEFINE VARIABLE loop AS INTEGER NO-UNDO.
DEFINE VARIABLE cnt AS INTEGER NO-UNDO.
// Define and populate temp-table
DEFINE TEMP-TABLE ttTagsToKeep NO-UNDO
FIELD TagName AS CHARACTER
INDEX idx1 AS PRIMARY UNIQUE TagName.
cnt = NUM-ENTRIES(cTagsToKeep).
DO loop = 1 to cnt:
CREATE ttTagsToKeep.
ASSIGN ttTagsToKeep.TagName = TRIM(ENTRY(loop, cTagsToKeep)).
END.
Now you can look for a matching record. Whether you clean up cRecord or do multiple CAN-FINDs is up to you.
IF CAN-FIND(ttTagsToKeep WHERE ttTagsToKeep.TagName EQ cRecord) THEN
DO:
// create ttTag record
END.
I have something like this in an input XML
<OrderText>
<text_type>0012</text_type>
<text_content>Text1</text_content>
</OrderText>
<OrderText>
<text_type>ZT03</text_type>
<text_content>Text2</text_content>
</OrderText>
The above data I need to map after concatenating as the below schema
<Order>
<Note>0012:Text1#ZT03:Text2</Note>
</Order>
Can anyone please help?
I'm going to assume that your input actually has a Root node, as otherwise it is not valid XML.
<Root>
<OrderText>
<text_type>0012</text_type>
<text_content>Text1</text_content>
</OrderText>
<OrderText>
<text_type>ZT03</text_type>
<text_content>Text2</text_content>
</OrderText>
</Root>
Then all you need is a map like this
With a String Concatenate functoid with
Input[0] = text_type
Input[1] = :
Input[2] = text_content
Input[3] = #
That goes into a Cumulative Concatenate
This will give you an output of
<Order>
<Note>0012:Text1#ZT03:Text2#</Note>
</Order>
Note: There is a extra # at the end, but you could use some more functoids to trim that off if needed.
You can use the Value-Mapping Flattening functoid in a map, then feed the result of each into a Concatenate functoid to generate the result string. The map can be executed on a port or in an orchestration.
I have written a program for export some text files to a specific directory. So i preferred using MTIME is the best way to have a unique name but this will be a problem when multiple process exporting same file name using MTIME.
Could you please tell me the best way to have a unique file name? Let me share some sample.
DEFINE INPUT PARAMETER ipData1 AS CHARACTER NO-UNDO.
DEFINE INPUT PARAMETER ipData2 AS CHARACTER NO-UNDO.
DEFINE INPUT PARAMETER ipData3 AS CHARACTER NO-UNDO.
DEFINE VARIABLE cExportData AS CHARACTER NO-UNDO FORMAT 'X(250)'.
DEFINE VARIABLE cPath AS CHARACTER NO-UNDO.
DEFINE VARIABLE cExt AS CHARACTER NO-UNDO.
DEFINE VARIABLE cSFTL AS CHARACTER NO-UNDO FORMAT 'X(150)'.
DEFINE VARIABLE cMessageDateTime AS CHARACTER NO-UNDO.
ASSIGN
cPath = "R:\Downloads\progress\"
cExt = ".Txt"
cMessageDateTime = "123456789".
OUTPUT TO VALUE (cPath + cMessageDateTime + STRING(MTIME) + cExt ).
put unformatted ipData1 skip ipData2 skip ipData3 skip "End."
OUTPUT CLOSE.
You have several options:
Use the program that Progress has supplied: adecomm/_tmpfile.p
define variable fname as character no-undo format "x(30)".
run adecomm/_tmpfile.p ( "xxx", ".tmp", output fname ).
display fname.
Use a GUID:
define variable fname as character no-undo format "x(30)".
fname = substitute( "&1&3&2", "xxx", ".tmp", GUID( GENERATE-UUID )).
display fname.
Ask Windows to do it (if you are always running on Windows):
define variable fname as character no-undo format "x(30)".
fname = System.IO.Path:GetTempFileName().
display fname.
Trial and error:
define variable fname as character no-undo.
do while true:
fname = substitute( "&1&3&2", "xxx", ".tmp", string( random( 1, 1000 ), "9999" )).
file-info:filename = fname.
if file-info:full-pathname = ? then leave. /* if the file does NOT exist it is ok to use this name */
end.
display fname.
You'll probably also need to pass in a token or an identifier of some sort to make this truly unique. Maybe username, or the machine's up, something like that. Then my advice would be concatenating that with
REPLACE (STRING(TODAY),'/','') + STRING(MTIME).
Edit: even though OP has flagged my answer as correct, it's not. Check Tom's answer to this for better options.
i have used this before.
("Filename" + STRING(TODAY,"999999") + ".csv").
Ho do I write code for a program that can accept three input parameters: x , y, and the filename to write to?
I should be able to call the program like this:
run prog.p (input “1”, input 5, input “filename1.csv”).
so far my I have written the code below and not sure how to go around it.
OUTPUT TO xxxxxx\filename1.csv".
DEFINE VARIABLE Profit AS DECIMAL FORMAT "->>,>>9.99":U INITIAL 0 NO-UNDO.
EXPORT DELIMITER "," "Amount" "Customer Number" "Invoice Date" "Invoice Number" "Total_Paid" "Profit".
FOR EACH Invoice WHERE Invoice.Ship-charge > 5.00
AND Invoice.Total-Paid > 0.01
AND Invoice.Invoice-Date GE 01/31/93 /* this is between also can use < >*/
AND Invoice.Invoice-Date LE TODAY NO-LOCK:
Profit = (Invoice.Invoice-Num / Invoice.Total-Paid) * 100.
EXPORT DELIMITER "," Amount Cust-Num Invoice-Date Invoice-Num Total-Paid Profit.
END.
OUTPUT CLOSE.
Thank you.
You're on the right track! OUTPUT TO VALUE(variable) is what might help you. Also you should possibly use a named stream.
It's not clear to me what parameters x and y should do so I just inserted them as dummies below.
Note:
You're commenting about using <> instead of GE. That might work logically but could (will) effect your performance by forcing the database to scan entires tables instead of using an index.
Something like this:
DEFINE INPUT PARAMETER pcX AS CHARACTER NO-UNDO.
DEFINE INPUT PARAMETER piY AS INTEGER NO-UNDO.
DEFINE INPUT PARAMETER pcFile AS CHARACTER NO-UNDO.
/* Bogus temp-table to make the program run... */
/* Remove this unless just testing without database ...*/
DEFINE TEMP-TABLE Invoice NO-UNDO
FIELD Ship-Charge AS DECIMAL
FIELD Total-Paid AS DECIMAL
FIELD Invoice-Date AS DATE
FIELD Invoice-Num AS INTEGER
FIELD Amount AS INTEGER
FIELD Cust-Num AS INTEGER.
DEFINE STREAM str.
DEFINE VARIABLE Profit AS DECIMAL FORMAT "->>,>>9.99":U INITIAL 0 NO-UNDO.
OUTPUT STREAM str TO VALUE(pcFile).
EXPORT STREAM str DELIMITER "," "Amount" "Customer Number" "Invoice Date" "Invoice Number" "Total_Paid" "Profit".
FOR EACH Invoice WHERE Invoice.Ship-charge > 5.00
AND Invoice.Total-Paid > 0.01
AND Invoice.Invoice-Date GE 01/31/93 /* this is between also can use < >*/
AND Invoice.Invoice-Date LE TODAY NO-LOCK:
Profit = (Invoice.Invoice-Num / Invoice.Total-Paid) * 100.
EXPORT STREAM str DELIMITER "," Amount Cust-Num Invoice-Date Invoice-Num Total-Paid Profit.
END.
OUTPUT STREAM str CLOSE.
Now you can run this program, assuming it's named "program.p":
RUN program.p("1", 5, "c:\temp\file.txt").
or
RUN program.p(INPUT "1", INPUT 5, INPUT "c:\temp\file.txt").
(INPUT is the default direction for parameters).
EDIT:
Run example + changed first input to CHARACTER instead of integer
Is there any way to pass a list of search strings in the contains() method of FilterExpression in DynamoDb?
Something like below:
search_str = ['value-1', 'value-2', 'value-3']
result = kb_table.scan(
FilterExpression="contains (title, :titleVal)",
ExpressionAttributeValues={ ":titleVal": search_str }
)
For now I can only think of looping through the list and scanning the table multiple times (as in below code), but I think it will be resource heavy.
for item in search_str:
result += kb_table.scan(
FilterExpression="contains (title, :titleVal)",
ExpressionAttributeValues={ ":titleVal": item }
)
Any suggestions.
For the above scenario, the CONTAINS should be used with OR condition. When you give array as input for CONTAINS, DynamoDB will check for the SET attribute ("SS", "NS", or "BS"). It doesn't looks for the sub-sequence on the string attribute.
If the target attribute of the comparison is of type String, then the
operator checks for a substring match. If the target attribute of the
comparison is of type Binary, then the operator looks for a
subsequence of the target that matches the input. If the target
attribute of the comparison is a set ("SS", "NS", or "BS"), then the
operator evaluates to true if it finds an exact match with any member
of the set.
Example:-
movies1 = "MyMovie"
movies2 = "Big New"
fe1 = Attr('title').contains(movies1)
fe2 = Attr('title').contains(movies2)
response = table.scan(
FilterExpression=fe1 or fe2
)
a little bit late but to allow people to find a solution i give here my method.
lets assume that in your DB you have a props called 'EMAIL you want to filter your scan on this EMAIL with a list of value. you can proceed as following.
list_of_elem=['mail1#mail.com','mail2#mail.com','mail3#mail.com']
#set an empty string to create your query
stringquery=""
# loop each element in your list
for index,value in enumerate(list_of_elem):
# add your query of contains with mail value
stringquery=stringquery+f"Attr('EMAIL').contains('{value }')"
# while your value is not the last element in list add the 'OR' operator
if index < len(list_of_elem)-1:
stringquery=stringquery+ ' | '
dynamodb = boto3.resource('dynamodb')
# Use eval of your query string to parse the string as filter expression
tableUser = dynamodb.Table('mytable')
tableUser.scan(
FilterExpression=eval(stringquery)
)