Related
The top right cell (Natwest) is a list from a range using data validation.
The Opening Balance 1,000.00 is sourced from another sheet using a lookup formula.
Using simple if statements, the cumulative balance is then produced - according to the Amount column and whether the Natwest account occurs in the Dr(+) or Cr (-) column
i.e. =if(B4=$D$1,D3+A4,if(C4=$D$1,D3-A4,D3)) and copied down.
Natwest
Amount Dr Cr Balance
1,000.00
100.00 Natwest Account 1 1,100.00
200.00 Account 2 Natwest 900.00
400.00 Natwest Account 1 1,300.00
It works fine, except that when a new row is inserted, the if statement formula is not copied into the new row.
I am looking for an arrayformula solution (or other formula inside the cell solution), so that the Cumulative Balance still works, but doesn't need to be copied into column D new row - when a new row(s) are inserted.
(I don't mind the Natwest (drop down from the list) or the Opening Balance 1,000.00 to be moved elsewhere if required for a solution.)
Thanks for your help.
Something adding up in between the same range of the arrayformula is always going to be tricky with circular dependency. I suggest to get the initial value and add it the SUMIF of second column and substract the SUMIF of second column up to each value. With BYROW you can do it like this:
=BYROW(A4:A,LAMBDA(each,SUMIF(INDIRECT("B4:B"&ROW(each)),D1,A4:each)-SUMIF(INDIRECT("C4:C"&ROW(each)),D1,A4:each)+D3))
Alternate solution:
You can use this custom function from AppScript for automatically calculating cumulative balance
Code:
function customFunction(startnum, key, range) {
var res = [];
var current = startnum;
range.forEach((x) => {
res.push(x.map((y, index) => {
return y == key && index == 1 ? current = (current + x[0]) : (y == key && index == 2 ? current = (current - x[0]) : null)
}).filter(c => c))
})
return res;
}
Custom Function Parameters:
=customFunction(startnum, key, range)
startnum = opening balance
key = Account name
range = cell range
Sample output:
=customFunction(D3,D1,A4:C)
I have a table with some data. The sheet in question does have empty rows.
Sheet1:
date
id
name
col1
col2
col3
2022-01-01
31
one
yes
1
5
2022-02-01
26
two
yes
2
4
2022-03-01
150
three
no
3
9
...
...
...
...
...
...
[empty row]
[empty row]
For each row, I have two metrics:
is good = col1 = yes
% complete = col2 / col3
I want to use an ARRAYFORMULA or something to split each row into two rows, one for each metric. So, for example, the expected output is:
date
id
name
metric
value
2022-01-01
31
one
is good
true
2022-01-01
31
one
% complete
.2
2022-02-01
26
two
is good
true
2022-02-01
26
two
% complete
.5
2022-02-01
150
three
is good
false
2022-02-01
150
three
% complete
.33
I came up with this formula, but it puts a lot of blank lines in the middle since the source data has empty rows.
=ARRAYFORMULA(
{
IF(
Sheet1!A2:A = "",
,
{
Sheet1!A2:C,
Sheet1!D2:D = "yes"
}
);
IF(
Sheet1!A2:A = "",
,
{
Sheet1!A2:C,
Sheet1!E2:E / Sheet1!F2:F
}
)
}
)
My source data has a lot of rows, so I'm trying to find an efficient way to do this. I also considered ARRAYFORMULA with column concatenation (like an unpivot) but it's horribly slow because I have so much data.
My source data also has a lot of columns that translate to 10 different metrics/rows.
I thought to try something like this so I'm only processing rows with data, but it results in a Formula parse error.
=ARRAYFORMULA(
IF(
Sheet1!A2:A = "",
,
{
Sheet1!A2:C,
Sheet1!D2:D = "yes";
Sheet1!A2:C,
Sheet1!D2:D = "yes";
}
)
)
Updated: I have created a sheet with my example data: https://docs.google.com/spreadsheets/d/1FgQ40zLd99f1xxkTtpwHStp91u5OFilyT_eigs0nYng/edit#gid=0.
parse error is a result of having a semicolon after 2nd yes. change:
="yes";}))
to
="yes"}))
pro tip: don't use such JS syntax highlighting
update 1:
=SORT({
FILTER({Sheet1!A2:C,
IFERROR(N(Sheet1!A2:A)/0, "is good"), Sheet1!D2:D="yes"}, Sheet1!A2:A<>"");
FILTER({Sheet1!A2:C,
IFERROR(N(Sheet1!A2:A)/0, "% complete"), Sheet1!E2:E/Sheet1!F2:F}, Sheet1!A2:A<>"")})
update 2:
=LAMBDA(a, b, c, d, e, SORT({
FILTER({a, IFERROR(N(b)/0, "is good"), c="yes"}, b<>"");
FILTER({a, IFERROR(N(b)/0, "% complete"), d/e}, b<>"")}))
(Sheet1!A2:C, Sheet1!A2:A, Sheet1!D2:D, Sheet1!E2:E, Sheet1!F2:F)
Use REDUCE to loop and create two rows for every row:
=REDUCE(
{Sheet1!A1:C1,"metric","value"},
Sheet1!A2:INDEX(Sheet1!A2:A,COUNTA(Sheet1!A2:A)),
LAMBDA(
a,c,
{a;
OFFSET(c,0,0,1,3),"is good", OFFSET(c,,3,1,1)="yes";
OFFSET(c,0,0,1,3),"% complete", QUERY(OFFSET(c,0,4,1,2),"select E/F label E/F ''",0)
}
)
)
I'm trying to make a formula that can recognize in Column A the name Brooke B for instance here, from there I'd like to SUM the values listed in Column I Cash Discounts for that specific user.
(Yes this user has no Cash Discounts, thus column I states "Non-Cash Payment").
There's about 80 users total here, so I'd prefer to automate the name recognition in Column A.
Sheet: https://docs.google.com/spreadsheets/d/1xzzHT7VjG24UJ4ZXaiZWsfzroTpn7jCJLexuTOf6SQs/edit?usp=sharing
Desired Results listed in Cash Discounts sheet, listed per user in column C.
You are trying to calculate the total amount of the Cash Discount per person given to people in a list. You have data that has been exported from a POS system to which that you have added a formula to calculate the amout of the discount on a line by line basis. You have speculated whether the discount totals could be calculated using SUMIFS formulae.
In my view, the layout of the spreadsheet and the format of the POS report do not lend themselves to isolating discrete data elements though Google sheets functions (though, no doubt, someone with greater skills than I will disprove this theory). Column A, containing names, also includes sub-groupings (and their sub-totals) as well as transaction dates. There are 83 unique persons and over 31,900 transaction lines.
This answer is a script-based solution which updates a sheet with the names and values of the discount totals. The elapsed execution time is #11 seconds.
function so5882893202() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// get the Discounts sheet
var discsheetname = "Discounts";
var disc = ss.getSheetByName(discsheetname);
//get the Discounts data
var discStartrow = 3;
var discLR = disc.getLastRow();
var discRange = disc.getRange(discStartrow, 1, discLR-discStartrow+1, 9);
var discValues = discRange.getValues();
// isolate Column A
var discnameCol = discValues.map(function(e){return e[0];});//[[e],[e],[e]]=>[e,e,e]
//Logger.log(discnameCol); // DEBUG
// isolate Column I
var discDiscounts = discValues.map(function(e){return e[8];});//[[e],[e],[e]]=>[e,e,e]
//Logger.log(discDiscounts); // DEBUG
// create an array to build a names list
var names =[]
// get the number of rows on the Discounts sheet
var discNumrows = discLR-discStartrow+1;
// Logger.log("DEBUG: number of rows = "+discNumrows);
// identify search terms
var searchPercent = "%";
var searchTotal = "Total";
// loop through Column A
for (var i=0; i<discNumrows; i++){
//Logger.log("DEBUG: i="+i+", content = "+discnameCol[i]);
// test if value is a date
if (Object.prototype.toString.call(discnameCol[i]) != "[object Date]") {
//Logger.log("it isn't a date")
// test whether the value contains a % sign
if ( discnameCol[i].indexOf(searchPercent) === -1){
//Logger.log("it doesn't have a % character in the content");
// test whether the value contains the word Total
if ( discnameCol[i].indexOf(searchTotal) === -1){
//Logger.log("it doesn't have the word total in the content");
// test whether the value is a blank
if (discnameCol[i] != ""){
//Logger.log("it isn't empty");
// this is a name; add it to the list
names.push(discnameCol[i])
}// end test for empty
}// end test for Total
} // end for percentage
} // end test for date
}// end for
//Logger.log(names);
// get the number of names
var numnames = names.length;
//Logger.log("DEBUG: number of names = "+numnames)
// create an array for the discount details
var discounts=[];
// loop through the names
for (var i=0;i<numnames;i++){
// Logger.log("DEBUG: name = "+names[i]);
// get the first row and last rows for this name
var startrow = discnameCol.indexOf(names[i]);
var endrow = discnameCol.lastIndexOf(names[i]+" Total:");
var x = 0;
var value = 0;
// Logger.log("name = "+names[i]+", start row ="+ startrow+", end row = "+endrow);
// loop through the Cash Discounts Column (Column I) for this name
// from the start row to the end row
for (var r = startrow; r<endrow;r++){
// get the vaue of the cell
value = discDiscounts[r];
// test that it is a value
if (!isNaN(value)){
// increment x by the value
x = +x+value;
// Logger.log("DEBUG: r = "+r+", value = "+value+", x = "+x);
}
}
// push the name and the total discount onto the array
discounts.push([names[i],x]);
}
//Logger.log(discounts)
// get the reporting sheet
var reportsheet = "Sheet10";
var report = ss.getSheetByName(reportsheet);
// define the range (allow row 1 for headers)
var reportRange = report.getRange(2,1,numnames,2);
// clear any existing content
reportRange.clearContent();
//update the values
reportRange.setValues(discounts);
}
Report Sheet - extract
Not everyone wants a script solution to their problem. This answer seeks to supply a repeatable solution using common garden-variety formula/functions.
As noted elsewhere, the layout of the spreadsheet does not lend itself to a quick/simple solution, but it IS possible to break down the data to compile a non-script answer. Though it may "seem" as though the following formula are less than "simple, when taken one-at-a-time they are logical, very easy to create, and very easy to verify successful outcomes.
Note: It is important to know at the outset that the first row of data = row#3, and the last row of data = row#31916.
Step#1 - get Text values from ColumnA
Enter this formula in Cell J3, and copy to row 31916
=if(isdate(A3),"",A3):
evaluates Column A, if the content is a date, returns blank, otherwise, returns the context
Taking Customer "AJ" as an example, the content at this point includes:
AJ
10% BuildingDiscount
10% BuildingDiscount Total:
Northwestern 10%
Northwestern 10% Total:
AJ Total:
Step#2 - ignore the values that contain "10%" (this removes both headings and sub-subtotals
Enter this formula in Cell K3 and copy to row 31916
=iferror(if(search("10%",J3)>0,"",J3),J3): searches for "10%" in Column J. Returns all values except those that containing "10%".
Taking Customer "AJ" as an example, the content at this point includes:
AJ
AJ Total:
**Step#3 - ignore the values that contain the word "Total"
Enter this formula in Cell L3 and copy to row 31916.
=iferror(if(search("total",K3)>0,"",K3),K3)
Taking Customer "AJ" as an example, the content at this point includes:
AJ
Results after Step#3
You might wonder, "couldn't this be done in a single formula?" and/or "an array formula would be more efficent". Both those thoughts are true, but we're looking at simple and easy, and a single formula is NOT simple (as shown below); and given that, an array formula is out-of-the-question unless/until an expert can wave a magic wand over the data.
FWIW - Combining Steps#1, 2 & 3
each of the Steps#1, 2 and 3 build on each other. So it is possible to create a single formula that combines these steps.
enter this formula in Cell J3, and copy dow to row #31916.
=iferror(if(search("total",iferror(if(search("10%",if(isdate(A3),"",A3))>0,"",if(isdate(A3),"",A3)),if(isdate(A3),"",A3)))>0,"",iferror(if(search("10%",if(isdate(A3),"",A3))>0,"",if(isdate(A3),"",A3)),if(isdate(A3),"",A3))),iferror(if(search("10%",if(isdate(A3),"",A3))>0,"",if(isdate(A3),"",A3)),if(isdate(A3),"",A3)))
As the image showed, step#3 concludes with mainly empty cells in Column L; the only populated cell is the first instance of the customer name at the start of their transactions - such as "Alec" in this example. However (props to #Rubén) it is possible to populate the blank transaction Cells in Column L. An arrayformula to find the previous non-empty cell in another column on Webapps explains how.
Step#4 - Create a customer name for each transaction row.
Enter this formula in Cell M3, it will automatically populate the cells to row#31916
=ArrayFormula(vlookup(ROW(3:31916),{IF(LEN(L3:L31916)>0,ROW(3:31916),""),L3:L31916},2))
Step#5 - Get the discount amount for each transaction value
The discount values are already displayed in Column I. They are interspersed with text values, so the formula for tests if this is a total line by testing the value in Column D; only if there is a vale (Product item) does the formula then test of there is a value in column I.
Enter this formula in Cell N3, it will automatically populate the cells to row#31916
=ArrayFormula(if(len(D3:D31914)>0,if(ISNUMBER(I3:I31916),I3:I31916,0),""))
Screenshot after step#5
Reporting by Query
Reporting is done via queries. These can go anywhere, but it is probably more convenient to put it on a separate sheet.
Step#6.1 - query the results to create report showing total by ALL customers
=query(Discounts_analysis!$M$2:$N$31916,"select M, sum(N) where N is not null group by M label M 'Customer', sum(N) 'Total Discount' ",1)
Step#6.2 - query the results to create report showing total by customer where the customer received a discount
=query(Discounts_analysis!$M$2:$N$31916,"select M, sum(N) where N >0 group by M label M 'Customer', sum(N) 'Total Discount' ",1)
Step#6.3 - query the results to create report showing customers with no discount
- `=query(query(Discounts_analysis!$M$2:$N$31916,"select M, sum(N) where N is not null group by M label M 'Customer', sum(N) 'Total Discount' ",1),"select Col1 where Col2=0")`
Queries screenshot
This may be a simple answer, I'm very new to the script editor in google sheets.
I have about 8 sheets for individual users to enter data. I would like a note showing the last modified date on a range of cells in each of the 8 sheets. Each of the 8 sheets is identical in the layout. With the code below, I'm able to get a last modified date note on column 2, row 26, and it works great. However, I can't figure out how to expand that to include columns 2-7 and rows 26-35. I assumed just entering 2:7 under range.getRow() would work, but it does not.
Any suggestions on how to achieve this? Even better, ideally having just one comment note in a single cell on the sheet(L24 as an example), that shows the last modified date of any cell edited between columns 2-8 and rows 26-35?
My goal is really just to identify the last time a user has modified any cells within that specified range.
Any suggestions would be greatly appreciated. The script is below.
/**
* The event handler triggered when editing the spreadsheet.
* #param {Event} e The onEdit event.
*/
function onEdit(e) {
// Set a comment on the edited cell to indicate when it was changed.
var range = e.range;
if(range.getRow() == 26 && range.getColumn() == 2){
range.setNote('ROW: ' + range.getRow() + " COLUMN: "+ e.range.getColumn());
range.setNote('Last modified: ' + new Date());
}}
The OP wants to create a note in cell L24 on any sheet where the edited cell is in the range B26:H35 (columns 2 to 8 inclusive).
The OP's own code could create a note in the edited cell but was only configured for edits in the range "B26". However, most of the logic was in place, and only modest changes are required.
The points to note in this answer are:
the Note is entered in cell "L24" of the edited sheet; this is made easier by defining a variable noterange.
the row and column numbers for the edited range are assigned to variables; this makes it easier to use when building the IF statement.
the edited range is assigned to a variable; this simplifies displaying the cell reference in the note.
the IF statement is in two parts: it tests whether i) the edited row is between 26 and 35 AND ii) the edited column is between 2 and 8.
the cell reference in the note is simplified by using getA1Notation.
I left the date as used by the OP, but this can be shortened/simplified
I inserted a line break between the range reference and "Date Modified" for easier reading
I created an Installable onEdit Trigger
function so54658181(e) {
// setup spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
// get the active sheet
var eactiveSheet = e.source.getActiveSheet();
// set the location for the Note
var noterange = eactiveSheet.getRange("L24");
// get the edited cell and find the row and column numbers
var erange = e.range;
var editedRow = erange.getRow();
var editedColumn = erange.getColumn()
// if the row is between 26 and 35, and the column is between 2 and 8, then set the note
if ((editedRow >= 26 && editedRow <= 35) && (editedColumn >= 2 && editedColumn <= 8)) {
noterange.setNote("Cell " + erange.getA1Notation() + String.fromCharCode(10) + "Last modified: " + new Date());
}
}
Formatting the Date
Add these two line anywhere above the IF statement
// set date format
var curDate = Utilities.formatDate(new Date(), "GMT+1", "E, MMM dd yyyy");
Edit the following line of the IF statement; note that it now references curDate
noterange.setNote("Latest edit: Cell " + erange.getA1Notation() + String.fromCharCode(10) + "On: " + curDate);
A link to the most recent Date and Time Patterns is included in the Google documentation.
I have a complex Google Sheet query that works great except when a Google Sheet doesn't have as many columns as I use in my formula.
Here's what the formula looks like now:
=sum(filter(query(INDIRECT("'" & A2 & "'!$A$7:$23"),"select Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH where B='"&C2&"'",0),query(INDIRECT("'" & A2 & "'!$A$7:$23"),"select Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH where B='PROJECT'",0) >=date(2017,1,1),query(INDIRECT("'" & A2 & "'!$A$7:$23"),"select Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH where B='PROJECT'",0) <=date(2017,12,31)))
It works great. But the problem is I run it against many worksheets and some don't have e.g. column AG,AH and end at AF at which point I get an error.
So what I need is a way to generate the string Q,R,S....[Name of Last Column in Sheet] and then I can use that instead of my hard-coded Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH but I cannot figure out how to do that.
Any help is greatly appreciated. Thanks!
Per comments above, final formula was:
LEFT("Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH,AI,AJ,AK,AL,AM,AN,AO,AP,AQ,AR,AS,AT,AU,AV,AW,AX,AY,AZ,BA,BB,BC ",2*Columns(INDIRECT(A2&"!1:1"))-33+IF(Columns(INDIRECT(A2&"!1:1"))>26,Columns(INDIRECT(A2&"!1:1"))-26,0))
where column A contains the list of worksheets (tabs) in the Google Sheet. Put this in B2, and then copied it down. I am not marking this as the correct answer since others gave a correct formula-based answer but this did the trick for me.
This can be done with built-in functions:
On a helper sheet, let say you name it, helper, fill up range with letters A to Z, let say A1:A26
Let say that on B1 you write the following formula:
=ArrayFormula({A1:A26;TRANSPOSE(SPLIT(JOIN(",",SUBSTITUTE(QUERY(TRANSPOSE(A1:A26)&A1:A26,,27)," ",",")),","))}) . This will create a list of column letter headers.
On each new worksheet use columns(1:1) to get the total number of columns.
To get your string of column headers, then you could use something like :
JOIN(",",OFFSET(helper!B1,16,0,columns(1:1)-16))
QUERY(helper!B:B,"select B limit "&columns(1:1)-7&" offset 7")
NOTE:
If you decide to have only one helper sheet and use it on several spreadsheets, then use
QUERY(IMPORTRANGE(your_url,"helper!B:B"),"select Col1 limit "&columns(1:1)-7&" offset 7")
This can be done with script. Without seeing you spreadsheet, it is hard to know exactly what you need, but this should be close. I get the variables from Sheet1 and return the formula to Sheet1. Adjust the sheet name to fit your needs. This will look at your data sheets based on the variable sheet name determine the last column. Determine the column letters and build the string the query needs. It then sets the new query formula. I added a menu to run it from.
function onOpen() {
SpreadsheetApp.getActiveSpreadsheet().addMenu(
'Create Data', [
{ name: 'Run', functionName: 'formula' },
]);
}
function formula(){
var ss= SpreadsheetApp.getActiveSpreadsheet()
var s=ss.getSheetByName("Sheet1") //sheet where variables are
var sheet=s.getRange("A2").getValue()//variable sheet name
var sel=makeString(sheet) //get the select string of column letters
//Create formula and return to Sheet1 A3
var f= s.getRange("A3").setFormula('=sum(filter(query(INDIRECT("\'" & A2 & "\'!$A$7:$23"),"select '+sel+' where B=\'"&C2&"\'",0),query(INDIRECT("\'" & A2 & "\'!$A$7:$23"),"select '+sel+' where B=\'PROJECT\'",0) >=date(2017,1,1),query(INDIRECT("\'" & A2 & "\'!$A$7:$23"),"select '+sel+' where B=\'PROJECT\'",0) <=date(2017,12,31)))')
}
function makeString(sht){
var ss= SpreadsheetApp.getActiveSpreadsheet()
var s=ss.getSheetByName(sht)
var lc=s.getLastColumn()
var rng=s.getRange(1, 17, 1, lc).getValues()
var str=''
var ltr=[]
for(var i=17;i<rng[0].length+1;i++){
ltr[i]= columnToLetter(i)
str=ltr.join(',')
}
var str1=str.substr(17)
return str1
}
function columnToLetter(column)
{
var temp, letter = '';
while (column > 0)
{
temp = (column - 1) % 26;
letter = String.fromCharCode(temp + 65) + letter;
column = (column - temp - 1) / 26;
}
return letter;
}
Let me know if you have any questions.