First all I'm very new to Progress 4GL and still trying to get my head around how nesting FOR EACH blocks works. I have the following two tables that I'm getting information out of, ivc_header and ivc_mchgs:
ivc_header
invoice_nbr | sold_to_cust_nbr | sold_to_cust_seq | invoice_amt
1000051 | 70 | 0 | $1,000
1000049 | 70 | 1 | $1,500
1000010 | 310 | 0 | $2,000
1000011 | 310 | 1 | $2,500
ivc_mchgs
invoice_nbr | line_nbr | misc_seq_nbr | extension
1000051 | 1 | 1 | $300
1000051 | 1 | 2 | $200
1000051 | 2 | 1 | $100
1000049 | 1 | 1 | $400
1000049 | 1 | 2 | $100
1000049 | 2 | 1 | $150
1000010 | 1 | 1 | $50
1000010 | 1 | 2 | $50
1000010 | 2 | 1 | $100
1000011 | 1 | 1 | $75
1000011 | 1 | 2 | $80
1000011 | 2 | 1 | $90
Just FYI, the primary key for ivc_header is invoice_nbr and for ivc_mchgs the primary is a composite key consisting of invoice_nbr, line_nbr, and misc_seq_nbr. The foreign key is invoice_nbr.
Just a note about the data, the information in ivc_mchgs are miscellaneous charges by invoice line_nbr.
What I'm trying to get is the total invoice_amt and extension by sold_to_cust_nbr + sold_to_cust seq. After doing some research I've decided to put the totals in variables instead of using Progress' built in ACCUMULATE function.
Here is the code that I have:
DEFINE VARIABLE cCustNum AS CHARACTER NO-UNDO.
DEFINE VARIABLE dInvoiceSubTotal AS DECIMAL NO-UNDO.
DEFINE VARIABLE dSurchargeTotal AS DECIMAL NO-UNDO.
FOR EACH ivc_header
NO-LOCK
WHERE (ivc_header.sold_to_cust_nbr = "000070")
OR (ivc_header.sold_to_cust_nbr = "000310")
BREAK BY ivc_header.sold_to_cust_nbr:
IF FIRST-OF(ivc_header.sold_to_cust_nbr) THEN
ASSIGN dInvoiceSubTotal = 0.
ASSIGN dInvoiceSUbTotal = dInvoiceSUbTotal + ivc_header.invoice_amt.
IF LAST-OF(ivc_header.sold_to_cust_nbr) THEN
DISPLAY ivc_header.sold_to_cust_nbr + ivc_header.sold_to_cust_seq FORMAT "x(9)" LABEL "CustNum"
dInvoiceSUbTotal LABEL "SubTotal".
FOR EACH ivc_mchgs WHERE ivc_header.invoice_nbr = ivc_mchgs.invoice_nbr
NO-LOCK
BREAK BY ivc_mchgs.invoice_nbr:
IF FIRST-OF(ivc_mchgs.invoice_nbr) THEN
ASSIGN dSurchargeTotal = 0.
ASSIGN dSurchargeTotal = dSurchargeTotal + ivc_mchgs.extension.
IF LAST-OF (ivc_mchgs.invoice_nbr) THEN
DISPLAY
dSurchargeTotal LABEL "Surcharge".
END.
END.
This code will give me the total invoice_amt by sold_to_cust_nbr + sold_to_cust_seq and totals the extension by invoice_nbr. What I can't figure out how to do is get a total of extension by sold_to_cust_nbr + sold_to_cust_seq.
Any help is appreciated.
Thanks
I think you might not be aware that you can specify multiple BY clauses.
IOW you might want to code the inner FOR EACH something like this:
FOR EACH ivc_mchgs NO-LOCK WHERE ivc_header.invoice_nbr = ivc_mchgs.invoice_nbr
BREAK BY ivc_mchgs.invoice_nbr
BY ivc_mchgs.sold_to_cust_nbr
BY ivc_mchgs.sold_to_cust_seq:
IF FIRST-OF(ivc_mchgs.invoice_nbr) THEN
ASSIGN dSurchargeTotal = 0.
IF FIRST-OF(ivc_mchgs.sold_to_cust_nbr ) THEN ...
etc.
On the assumption you want both the exchange total by invoice and summary, then you could just do this:
DEFINE VARIABLE cCustNum AS CHARACTER NO-UNDO.
DEFINE VARIABLE dInvoiceSubTotal AS DECIMAL NO-UNDO.
DEFINE VARIABLE dSurchargeTotal AS DECIMAL NO-UNDO.
DEFINE VARIABLE dSurchargeSubTl AS DECIMAL NO-UNDO.
FOR EACH ivc_header
NO-LOCK
WHERE (ivc_header.sold_to_cust_nbr = "000070")
OR (ivc_header.sold_to_cust_nbr = "000310")
BREAK BY ivc_header.sold_to_cust_nbr:
IF FIRST-OF(ivc_header.sold_to_cust_nbr) THEN
ASSIGN dInvoiceSubTotal = 0
dSurchargeSubTl = 0.
ASSIGN dInvoiceSUbTotal = dInvoiceSUbTotal + ivc_header.invoice_amt.
IF LAST-OF(ivc_header.sold_to_cust_nbr) THEN
DISPLAY ivc_header.sold_to_cust_nbr + ivc_header.sold_to_cust_seq FORMAT "x(9)" LABEL "CustNum"
dInvoiceSUbTotal LABEL "SubTotal"
dSurchargeSubTL LABEL "Srchg SubTl".
FOR EACH ivc_mchgs WHERE ivc_header.invoice_nbr = ivc_mchgs.invoice_nbr
NO-LOCK
BREAK BY ivc_mchgs.invoice_nbr:
IF FIRST-OF(ivc_mchgs.invoice_nbr) THEN
ASSIGN dSurchargeTotal = 0.
ASSIGN dSurchargeTotal = dSurchargeTotal + ivc_mchgs.extension.
IF LAST-OF (ivc_mchgs.invoice_nbr) THEN DO:
DISPLAY dSurchargeTotal LABEL "Surcharge".
ASSIGN dSurchargeSubTl = dSurchargeSubTl + dSurchargeTotal.
END.
END.
END.
The elegant way would be to combine both queries using a left outer join, and use the ACCUMULATE functions, but this should work.
Related
How can I use indexed variable in a Firebird stored procedure? I mean, I have output parameters ODATE1, ODATE2, ODATE3, can I use as ':ODATE' || COUNTER to set the value in a loop?
I have 2 tables like this:
1. T_EMPLOYEE
---------------
| ID_E | NAME |
---------------
| 1 | Anne |
---------------
| 2 | Bob |
---------------
2. T_WORK
----------------------------
| ID_W | DATE_W | ID_E |
----------------------------
| 1 | 2021-01-01 | 1 |
----------------------------
| 2 | 2021-01-01 | 2 |
----------------------------
| 3 | 2021-01-02 | 1 |
----------------------------
| 4 | 2021-01-03 | 2 |
----------------------------
From that table I want to make a stored procedure to get this result:
DASHBOARD
-----------------------------------------------------------
| OID_E | ONAME | ODATE1 | ODATE2 | ODATE3 |
----------------------------------------------------------
| 1 | Anne | 1 | 1 | 0 |
-----------------------------------------------------------
| 2 | Bob | 1 | 0 | 1 |
-----------------------------------------------------------
I tried using EXECUTE STATEMENT like this in stored procedure:
DECLARE VARIABLE COUNTER INT;
BEGIN
FOR
SELECT ID_E, NAME FROM T_EMPLOYEE
INTO :OID_E, :ONAME
D0
BEGIN
COUNTER = 1;
WHILE (COUNTER<=3) DO
BEGIN
EXECUTE STATEMENT 'SELECT COUNT(*) FROM T_WORK WHERE DATE_W = ''2021-01-0' || COUNTER ||
''' AND ID_E = ' || :OID_E || ' INTO :ODATE' || COUNTER;
COUNTER = COUNTER + 1;
END
SUSPEND;
END
END /*procedure*/
The procedure can't be compiled.
Then I tried the simple one like this without COUNTER index replacement:
DECLARE VARIABLE COUNTER INT;
BEGIN
FOR
SELECT ID_E, NAME FROM T_EMPLOYEE
INTO :OID_E, :ONAME
D0
BEGIN
COUNTER = 1;
WHILE (COUNTER<=3) DO
BEGIN
EXECUTE STATEMENT 'SELECT COUNT(*) FROM T_WORK WHERE ID_E = :OID_E ' ||
' AND DATE_W =''2021-01-02'' INTO :ODATE2';
COUNTER = COUNTER + 1;
END
SUSPEND;
END
END /*procedure*/
The procedure can be compiled, but when I execute, it will raise this error:
SQL Error: Dynamic SQL Error SQL error code = #1 Token unknown - line #1, column #2 #1. Error Code: -104. Invalid token
Please give me insight. How to use EXECUTE STATEMENT to make a flexible looping to set indexed variable.
Or you have another solution for my needs.
Additional information: Firebird v2.5
You cannot dynamically reference PSQL variables (including parameters) like this. However, you don't need to jump through all these hoops to get the desired results.
You can use something like the following(which doesn't even need a procedure):
select e.ID_E as OID_E, e.NAME as ONAME,
count(case when w.DATE_W = date '2021-01-01' then 1 end) as ODATE1,
count(case when w.DATE_W = date '2021-01-02' then 1 end) as ODATE2,
count(case when w.DATE_W = date '2021-01-03' then 1 end) as ODATE3
from T_EMPLOYEE e
inner join T_WORK w
on w.ID_E = e.ID_E
group by e.ID_E, e.NAME
order by e.ID_E
Fiddle: https://dbfiddle.uk/?rdbms=firebird_3.0&fiddle=59066afc0fd7f6b5cb87eac99164e899
Given a sheet like this:
+ ------ + ------- + ---------- + ---------- + ---------- + ---------- +
| A | B | C | D | E | F |
+ -------+ ------- + ---------- + ---------- + ---------- + ---------- +
| AVG | ITEMS | Week 3 May | Week 2 May | Week 1 May | Week 5 Apr |
|=QUERY()| Item 1 | 1263 | 1255 | 1142 | 956 |
| | Item 2 | 1371 | 1263 | 1023 | 1120 |
| | Item 3 | 1382 | 1257 | 1352 | 1853 |
| | Item 4 | 1429 | 1281 | 1120 | 1869 |
I need to move the column B to the first column (A).
Make a script to add a new colum for new entries.
1.-
In AVG column (column A in the example above) there is an average using the formula:
=QUERY(transpose(query(transpose(B2:$F),"Select "®EXREPLACE(join("",ArrayFormula(if(len(B2:B),"Avg(Col"&ROW($C2:$C)-ROW($C2)+1&"),",""))), ".\z","")&"")),"Select Col2")
This formula calculates the average of the last 4 weeks only if there's an entry in column B
I need to move this column to the right of the Items list (column B) but when I try to, the formula shows a circular dependency error. Is there a way to tell the formula to only pick the columns I want to?
2.-
There's also a button with an assigned macro to make a new column on the left of the latest week for new entries and insert the week number and month, this is the script:
function onEdit() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('C:C').activate();
spreadsheet.getActiveSheet().insertColumnsBefore(spreadsheet.getActiveRange().getColumn(), 1);
spreadsheet.getActiveRange().offset(0, 0, spreadsheet.getActiveRange().getNumRows(), 1).activate();
spreadsheet.getRange('C1').activate()
.setFormula('=CONCATENATE("Week ",(WEEKNUM(TODAY(),2)-WEEKNUM(EOMONTH(TODAY(),-1)+1)+1)," ",CHOOSE(MONTH(TODAY()),"Jan","Feb","Mar","Apr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dec"))');
};
So it becomes something like this:
+ ------ + ------- + ---------- + ---------- + ---------- + ---------- + ---------- +
| A | B | C | D | E | F | G |
+ -------+ ------- + ---------- + ---------- + ---------- + ---------- + ---------- +
| AVG | ITEMS | Week X MMM | Week 3 May | Week 2 May | Week 1 May | Week 5 Apr |
| | Item 1 | (NEW WEEK) | 1263 | 1255 | 1142 | 956 |
and this is the formula I am using for the week number:
=CONCATENATE("Week ",(WEEKNUM(TODAY(),2)-WEEKNUM(EOMONTH(TODAY(),-1)+1)+1)," ",CHOOSE(MONTH(TODAY()),"Jan","Feb","Mar","Apr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dec"))
The problem with the formula is that uses TODAY() function, which has a variable value while I need a static value. Also when using the script the Conditional Formatting is not carried over in the new column. How can I improve the script?
For replacing TODAY() with a static value, use the Javascript date methods and format the date according to your spreadsheet setting with Utilities.formatDate()
Replace the formula part TODAY() with the variable containing the date object as explained above
Sample:
function onEdit() {
var spreadsheet = SpreadsheetApp.getActive();
var now = new Date();
var today = '"' + Utilities.formatDate(now, spreadsheet.getSpreadsheetTimeZone(), "MM/dd/yyyy") + '"';
spreadsheet.getRange('C:C').activate();
spreadsheet.getActiveSheet().insertColumnsBefore(spreadsheet.getActiveRange().getColumn(), 1);
spreadsheet.getActiveRange().offset(0, 0, spreadsheet.getActiveRange().getNumRows(), 1).activate();
spreadsheet.getRange('C1').activate()
.setFormula('=CONCATENATE("Week ",(WEEKNUM(' +today + ',2)-WEEKNUM(EOMONTH(' + today + ',-1)+1)+1)," ",CHOOSE(MONTH(' + today + '),"Jan","Feb","Mar","Apr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dec"))');
};
Note: If your spreadsheet date format is no "MM/dd/yyyy" - modify formatDate(date, timeZone, format) accordingly, see here
The conditional formatting has to be set separately from setFormula, see here for a sample.
Let's say I have the following in a table :
A | B | desired_output
----------------------------
1 | 10 | 1 | 0
2 | 20 | 7 | 0
3 | 30 | 3 | 0
4 | 20 | 2 | 0
5 | 30 | 5 | 1
I'd like to find a formula for each of the cells in the desired_output column which looks at the max of B1:B5 but only for rows for which A = max(A1:A5)
If that's not clear, I'll try to put it another way :
for all the rows in A1:A5 that are equal to max(A1:A5) // so that's rows 3 and 5
find the one which has the max value on B // so between B3 and B5, that's B5
output 1 for this one, 0 for the other
I'd say there would be a where somewhere if such a function existed, something like = if(B=(max(B1:B5) where A = max(A1:A5)), 1, 0) but I can't find how to do it...
I can do it in two columns with a trick :
A | B | C | D
----------------------------
1 | 10 | 1 | | 0
2 | 20 | 7 | | 0
3 | 30 | 3 | 3 | 0
4 | 20 | 2 | | 0
5 | 30 | 5 | 5 | 1
With Cn = if(An=max(A$1:A$5),Bn,"") and Dn = if(Cn = max(C$1:C$5), 1, 0)
But I still can't find how to do it in one column
For systems without MAXIFS, put this in C1 and fill down.
=--(B1=MAX(INDEX(B$1:B$5-(A$1:A$5<>MAX(A$1:A$5))*1E+99, , )))
=ARRAYFORMULA(IF(LEN(A1:A), IF(IFERROR(VLOOKUP(CONCAT(A1:A&"×", B1:B),
JOIN("×", QUERY(A1:B, "order by A desc, B desc limit 1")), 1, 0), )<>"", 1, 0), ))
or shorter:
=ARRAYFORMULA(IF(A:A<>"",N(A:A&"×"&B:B=JOIN("×",SORTN(A:B,1,,1,0,2,0))),))
=ARRAYFORMULA(IF(A:A<>"",N(A:A&B:B=JOIN(,SORTN(A:B,1,,1,0,2,0))),))
How about the following:
=--AND(A5=MAX($A$1:$A$5),B5=MAXIFS($B$1:$B$5,$A$1:$A$5,MAX($A$1:$A$5)))
I'm trying to make two SUMs on the same column.
Here's my columns:
| 1-2 | 1 |
| 2 | 2-3 |
| 1 | 5 |
|-------|-------|
| 4 | 8 | Sum 1 that take the "min" value of each cells
| 5 | 9 | Sum 2 that take the "max" value of each cells
Sum 1 Column 1 : 1 + 2 + 1 = 4
Sum 2 Column 1 : 2 + 2 + 1 = 5
The cells notation is either {num} which is an absolute value, or {min}-{max} which is the min and max value
This is to create some work timing estimations and we would like to have this "min-max" concept. We have already something with split columns, but it will be more comfortable to keep 1 column with 2 possible values in each cells.
For the min:
=ArrayFormula(SUM(--(IFERROR(LEFT(A1:A3,FIND("-",A1:A3)-1),A1:A3))))
For the Max:
=ArrayFormula(SUM(--(IFERROR(RIGHT(A1:A3,len(A1:A3)-FIND("-",A1:A3)),A1:A3))))
Can not understand why aggregateQuery always returns an empty result. Tried to test in aql, the same problem: 0 rows in set.
Indexes are all there.
aql> show indexes
+---------------+-------------+-----------+------------+-------+------------------------------+-------------+------------+-----------+
| ns | bin | indextype | set | state | indexname | path | sync_state | type |
+---------------+-------------+-----------+------------+-------+------------------------------+-------------+------------+-----------+
| "test" | "name" | "NONE" | "profiles" | "RW" | "inx_test_name" | "name" | "synced" | "STRING" |
| "test" | "age" | "NONE" | "profiles" | "RW" | "inx_test_age" | "age" | "synced" | "NUMERIC" |
aql> select * from test.profiles
+---------+-----+
| name | age |
+---------+-----+
| "Sally" | 19 |
| 20 | |
| 22 | |
| 28 | |
| "Ann" | 22 |
| "Bob" | 22 |
| "Tammy" | 22 |
| "Ricky" | 20 |
| 22 | |
| 19 | |
+---------+-----+
10 rows in set (0.026 secs)
aql> AGGREGATE mystream.avg_age() ON test.profiles WHERE age BETWEEN 20 and 29
0 rows in set (0.004 secs)
It seems that you are trying the example here.
There are two problems about the udf script. I paste the code of the lua script :
function avg_age(stream)
local function female(rec)
return rec.gender == "F"
end
local function name_age(rec)
return map{ name=rec.name, age=rec.age }
end
local function eldest(p1, p2)
if p1.age > p2.age then
return p1
else
return p2
end
end
return stream : filter(female) : map(name_age) : reduce(eldest)
end
First, there is no bin named 'gender' in your set, so you got 0 rows after aggregateQuery.
Second, this script isn't doing exactly what the function name 'avg_age' means, it just return the eldest record with name and age.
I paste my code bellow, it just replace the reduce func, and alert the map and filter func to meat the demand. You can just skip the filter process.
function avg_age(stream)
count = 0
sum = 0
local function female(rec)
return true
end
local function name_age(rec)
return rec.age
end
local function avg(p1, p2)
count = count + 1
sum = sum + p2
return sum / count
end
return stream : filter(female) : map(name_age) : reduce(avg)
end
The output looks like bellow :
AGGREGATE mystream.avg_age() ON test.avgage WHERE age BETWEEN 20 and 29
+---------+
| avg_age |
+---------+
| 22 |
+---------+
1 row in set (0.001 secs)