I'm trying to expand the following formula down a column using an array, which I understand doesn't work exactly with countif. I need this as the divisor in the column where I have the numerator expanding successfully down the column. Here is the function that works fine in each row:
=arrayformula(countifs(unique(if('All Data'!$B$2:$B=$A2,'All Data'!$A$2:$A,)),">="&edate(F$2,0),unique(if('All Data'!$B$2:$B=$A2,'All Data'!$A$2:$A,)),"<="&edate(F$2,1)))
I am trying to avoid copying the formulas all over the sheet, as it is rather large and would prefer it to work as an array where it gives me the count for a2 in row 2, a3 in row 3, a4 in row 4, etc, so I tried this formula, which doesn't work:
=arrayformula(countifs(unique(if('All Data'!$B$2:$B=$A2:$A,'All Data'!$A$2:$A,)),">="&edate(F$2,0),unique(if('All Data'!$B$2:$B=$A2:$A,'All Data'!$A$2:$A,)),"<="&edate(F$2,1)))
Is there a workaround I can use to get this expanding for me?
Thank you!
You could accomplish this with an Apps Script custom function. To achieve this, follow these steps:
In your spreadsheet, select Tools > Script editor to open a script bound to your file.
Copy this function in the script editor, and save the project:
function AVERAGE_STUDENTS(classes, monthIndex) {
const sheet = SpreadsheetApp.getActive().getSheetByName("All Data");
const data = sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn()).getValues();
const monthRows = data.filter(row => row[0].getMonth() + 1 == monthIndex);
return classes.map(classy => {
const classRows = monthRows.filter(row => row[1] == classy[0]);
const totalStudents = classRows.length;
const uniqueDates = new Set(classRows.map(element => JSON.stringify([element[0], element[1]]))).size;
return totalStudents / uniqueDates;
});
}
Now, if you go back to your spreadsheet, you can use this function like any in-built one. You just have to provide the appropriate range for the classes (in this case it would be A3:A18) and the month index (for April, that's 4), as you can see here:
Note:
This function could also be modified in order to just call the function at C1 and populate all months automatically.
Reference:
Custom Functions in Google Sheets
Related
I want to extract subset of a range from another range by selecting starting columns and length.
For example, how can I extract B1:C1 from another range A1:D1 by indicating starting columns 2 as in the image attached?
Thanks!
In your question, how about the following answer? In this answer, I used a custom function created by Google Apps Script. The sample script is as follows.
Sample script:
Please copy and paste the following script to the script editor of Spreadsheet and save the script.
function someKindOfFunction(range, startColumn, length) {
const sheet = SpreadsheetApp.getActiveSheet();
const orgRange = sheet.getRange(range);
const srcRange = orgRange.offset(0, startColumn - 1, orgRange.getNumRows(), length);
const values = srcRange.getValues();
return values;
}
When you use this script, for your showing sample Spreadsheet, please put a custom function of =SUM(someKindOfFunction("A1:D1",2,2)) to a cell. By this, 5 is returned.
Testing:
When this script is used for your showing sample Spreadsheet, the following result is obtained.
In this case, the range is given as a string like =SUM(someKindOfFunction("A1:D1",2,2)). Please be careful about this.
Note:
At your showing image, you are using =SUM(someKindOfFunction(A1:D1,startingrow: 2, length: 2)).
From this, I guessed someKindOfFunction might be a custom function.
And, from your question, I guessed "startingrow" might be "startingcolumn".
Added:
As another direction, when you use A1:D1 as the range instead of the string value, how about the following sample script? In this sample script, the array is directly used. But, I'm not sure whether this is your expected direction.
function someKindOfFunction(values, startColumn, length) {
return values.map(r =>
r.flatMap((c, j) => {
const start = startColumn - 1;
return j >= start && j < start + length ? c : [];
})
);
}
In this case, when you put a custom function of =SUM(someKindOfFunction(A1:D1,2,2)) to a cell using your sample Spreadsheet, 5 is returned.
References:
Custom Functions in Google Sheets
offset(rowOffset, columnOffset, numRows, numColumns)
This is easily achievable with the OFFSET function:
=sum(offset(Range,,StartCol-1,,Width))
So for your specific example, the expression would be:
=sum(offset(A1:D1,,2-1,,2))
I use importrange for combine 2 google sheet Target A and Source B
In sheet A =
Importrange(“linkgooglesheetB”,”SheetB!!A2:M1000)
It’s done
But when data in sheet Source B cleared/changed, then
Data in sheet Target A cleared/changed too
What formula can use same my above formula but data in Target A do not change after combined (A&B), when B changed.
Answer:
You can do this with Google Apps Script.
Explanation:
The IMPORTRANGE formula will always automatically update when the range from which it is importing had a data change. In fact, this is a feature of all sheet formulae; they are designed to keep everything up-to-date when data changes.
For this reason, you can not use a formula. You will have to do this using Google Apps Script.
Example code:
The idea is as such:
When the sheet is edited, a script will run.
If the cell data matches a certain pattern, then the script will paste in the data permanently, in the same way that IMPORTRANGE works.
function runOnEdit(e) {
// We will make the patten here. In this case, the text entered in the cell must be of the form:
// "IMPORT(link,range)
// example:
// IMPORT,https://docs.google.com/spreadsheets/d/some-id/edit, SheetB!A2:M1000
const patternStart = "IMPORT"
const compareVals = e.value.split(",")
if (compareVals.length != 3 || compareVals[0] != patternStart) return
try {
const ss = SpreadsheetApp.openByUrl(compareVals[1])
const importRange = compareVals[2].split('!')
const sheetName = importRange[0]
const range = importRange[1]
const sheet = ss.getSheetByName(sheetName)
console.log(sheetName)
console.log(range)
const data = sheet.getRange(range).getValues()
const formatting = sheet.getRange(range).getTextStyles()
const activeSheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet()
const row = e.range.getRow()
const col = e.range.getColumn()
activeSheet.getRange(row, col, data.length, data[0].length).setValues(data).setTextStyles(formatting)
}
catch (err) {
e.range.setValue(err)
}
}
Things to note:
The pattern can not start with an = as this will be read as a formula by Sheets and throw an error.
This must be set up as an installable trigger, as opening a separate Spreadsheet requires authorisation, which can not be done from a simple trigger.
You can install the trigger by clicking on the clock logo in the left toolbar from the Apps Script editor, then clicking + Add Trigger in the bottom right and using the following settings:
Choose which function to run: runOnEdit
Which runs at deployment: Head
Select event source: From spreadsheet
Select event type: On edit
Visual example:
Most of the formula is live update, including query function. However in order not to have your data change for importrange, there is two possible solution:
1)Create a backup sheet by copy paste data from the main sheet, and importrange from the backup sheet, so any change in main sheet will not have impact on your result
2)Write Google App Scrip to copy data, but it take time to learn
I have a row which look like this:
|Data A-1|Data A-2|Data A-3|Data A-4|Data A-5|Data A-6|Data B-1|Data B-2|Data B-3|Data B-4|Data B-5|Data B-6|Data C-1|Data C-2|Data C-3|Data C-4|Data C-5|Data C-6|
There are 5 columns that are related to each Data. I need to convert all the data in a row into fix rows and columns which look like this:
|Data A-1|Data A-2|Data A-3|Data A-4|Data A-5|Data A-6|
|Data B-1|Data B-2|Data B-3|Data B-4|Data B-5|Data B-6|
|Data C-1|Data C-2|Data C-3|Data C-4|Data C-5|Data C-6|
How can I achieve this in Google Sheets?
Example Sheet
In your case, I thought that this thread might be able to be used.
Sample formula 1:
For the goal in your question, how about the following sample formula?
=ARRAYFORMULA(TRIM(SPLIT(TRANSPOSE(SPLIT(REGEXREPLACE(TEXTJOIN(",",TRUE,A1:1),"(([\w\s\S]+?,){6})","$1#"),"#")),",")))
In this case, one row is used. So A1:1 is used as the range. But when you have several rows, please modify the range. And, the row is splitted by 6 column. So (([\w\s\S]+?,){6}) is used as the regex.
The flow of this formula is as follows.
Join all cell values by ignoring the empty cells using TEXTJOIN.
Put # to the joined text value for 6 columns using REGEXREPLACE.
Split the text value with # using SPLIT.
Transpose the splitted values using TRANSPOSE.
Split the each row with , using SPLIT.
Result:
Sample formula 2:
For your shared Spreadsheet, how about the following sample formula?
=ARRAYFORMULA(TRIM(SPLIT(TRANSPOSE(SPLIT(REGEXREPLACE(TEXTJOIN(",",TRUE,A8:8),"(([\w\s\S]+?,){8})","$1#"),"#")),",")))
In this case, one row is used. So A8:8 is used as the range. But when you have several rows, please modify the range. And, the row is splitted by 8 column. So (([\w\s\S]+?,){8}) is used as the regex.
Result:
Note:
In this case, because of REGEXREPLACE(TEXTJOIN(",",TRUE,A8:8),"(([\w\s\S]+?,){8})","$1#"), when the characters are over 50,000, an error occurs. In that case, I would like to propose to use the Google Apps Script as the custom function. The sample script is as follows. Please copy and paste the following script to the script editor of Spreadsheet, and put =SAMPLE(A8:8, 8) to a cell when your shared Spreadsheet is used. In this case, the arguments of A8:8 and 8 are the range and the splitted number, respectively. By this, your goal can be achieved.
const SAMPLE = (values, split) => values.flatMap(r => {
const temp = [];
while (r.length > 0) temp.push(r.splice(0, split));
return temp
});
References:
TEXTJOIN
REGEXREPLACE
SPLIT
TRANSPOSE
Custom Functions in Google Sheets
Added:
About your additional question by your comment as follows,
Is there any way to make the first column to be sorted?
how about the following sample formula?
Sample formula:
=SORT(ARRAYFORMULA(TRIM(SPLIT(TRANSPOSE(SPLIT(REGEXREPLACE(TEXTJOIN(",",TRUE,A8:8),"(([\w\s\S]+?,){8})","$1#"),"#")),","))),1,TRUE)
In this case, the rows are sorted by the 1st column as the ascending order.
When you use the custom formula created by Google Apps Script, you can also use SORT as follows.
=SORT(SAMPLE(A8:8, 8),1,TRUE)
Or, you can also use the following script. When you use this, please put =SAMPLE2(A8:8, 8) to a cell.
const SAMPLE2 = (values, split) => values.flatMap(r => {
const temp = [];
while (r.length > 0) temp.push(r.splice(0, split));
return temp.sort((a, b) => a[0] - b[0]);
});
or try:
=QUERY(
{FLATTEN(FILTER(A8:15, MOD(COLUMN(A1:1)-1, 8)=0)),
FLATTEN(FILTER(A8:15, MOD(COLUMN(A1:1)-2, 8)=0)),
FLATTEN(FILTER(A8:15, MOD(COLUMN(A1:1)-3, 8)=0)),
FLATTEN(FILTER(A8:15, MOD(COLUMN(A1:1)-4, 8)=0)),
FLATTEN(FILTER(A8:15, MOD(COLUMN(A1:1)-5, 8)=0)),
FLATTEN(FILTER(A8:15, MOD(COLUMN(A1:1)-6, 8)=0)),
FLATTEN(FILTER(A8:15, MOD(COLUMN(A1:1)-7, 8)=0)),
FLATTEN(FILTER(A8:15, MOD(COLUMN(A1:1)-8, 8)=0))},
"where Col1 is not null", 0)
I am working on adding the time I spend on my habits using google sheets. If you look at this example sheet, I am keeping my individual habits in columns 3-8 (see the offsets on the first row).
To add the food related habits times (columns 5 and 6), I can use the range in offset function (see formulae in D17 below "Food").
The question is: how do I add the numbers for exercise and sleep (column offsets 4, 7, and 8)? The number of columns here could be 2, 3, or more! And they might not be consecutive.
Thanks for any pointers.
To sum entries of the rows whose columns are in the given array, I would use
=SUMPRODUCT(COUNTIF({5,8,9},COLUMN(D3:J3))*(D3:J3))
This is the formula for E18 in your spreasheet.
Since the columns might not be consecutive and there can be a variable number of them, I think it is appropriate to use an Apps Script custom function, and use the spread syntax to account for the variable number of columns.
Just open the script bound to your file, copy this function and save the project:
function HABIT_TOTALS(...habitIndexes) {
const sheet = SpreadsheetApp.getActiveSheet();
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
let output = [];
for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
let dayValue = 0;
habitIndexes.forEach(habitIndex => {
const columnIndex = headers.indexOf(habitIndex) + 1;
const dailyHabitValue = sheet.getRange(3, columnIndex).getValue();
const dayHabitValue = sheet.getRange(4 + dayIndex, columnIndex).getValue();
dayValue = Number(dayValue) + Number(dailyHabitValue) + Number(dayHabitValue);
});
output.push([dayValue]);
}
return output;
}
Notes:
This function can be used as any in-built formula from Sheets (e.g. =HABIT_TOTALS(4,7,8)).
This function gets, as arguments, the indexes of the habits to retrieve (in this case 4, 7, 8), to be found on the first row in the sheet.
It loops through all days of the week (dayIndex), returning the total amount for each day. Because of this, there's no need to drag the formula down.
For each day, it finds the column index based on the habit index provided as an argument, and adds the values for Daily and for the current day to the total value for the day.
After retrieving the total amount for the day, this value is pushed to output, the value returned by this function.
This function could be used for the Food habits, just changing the arguments: =HABIT_TOTALS(5,6), or for any other combination.
Reference:
Custom Functions in Google Sheets
Spread syntax (...)
For the calculation concerning food you can try in cell D18
=sum(filter(filter($D$3:$I$11, regexmatch($C$3:$C$11, "Daily|"&text($C18, "ddd"))), regexmatch($D$1:$I$1&"", "5|6")))
and fill down.
The numbers at the end refer to the colum numbers you have in row 1. So in E18 (Sleep and excercise) you would have
=sum(filter(filter($D$3:$I$11, regexmatch($C$3:$C$11, "Daily|"&text($C18, "ddd"))), regexmatch($D$1:$I$1&"", "4|7|8")))
Of course, it is also possible to write the last part in a cell and then refer to that cell. That would mean you can enter in E18
=sum(filter(filter($D$3:$I$11, regexmatch($C$3:$C$11, "Daily|"&text($C18, "ddd"))), regexmatch($D$1:$I$1&"", D$17)))
and fill down AND to the right.
See if that helps?
I'm new to this,
I have 2 google spreadsheets:
Spreadsheet A: The active sheet Containing multiple tabs with information to be Pushed to B.
Spreadsheet B: A spreadsheet with a single tab. The same headers and structure as spreadsheet A.
Based on the user selecting the answer "Yes" in the first column of any of the 1 tabs in Spreadsheet A, I would like that entire row to move over to Spreadsheet B.
I have modified a script that works on a single spreadsheet (ie moving rows from tab to tab) to attempt to get it to work between spreadsheets:
function onEdit(event) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var tss = SpreadsheetApp.openById('B').getSheetByName('Sheet 1');
var s = event.source.getActiveSheet();
var r = event.source.getActiveRange();
if(r.getColumn() == 1 && r.getValue() == "Yes") {
var row = r.getRow();
var numColumns = s.getLastColumn();
var target = tss.getRange(targetSheet.getLastRow() + 1, 1);
s.getRange(row, 1, 1, numColumns).moveTo(target);
}
}
Probably needless to say, this yields no result. Having searched through a number of posts and forums I only see individuals posting about how to move rows between tabs but not between entirely separate spreadsheets. Is it even possible to do this? If so, what am I doing wrong in the script?
Thank you so much for anyone who takes the time to assist.
Anthony
Following a dialogue with the OP in the comments section, it was indicated that the original spreadsheet did not to be kept secret.
Consequently, the desired functionality can be provided by using a combination of IMPORTRANGE() and QUERY() in the spreadsheet with no need to use Google App Script. For instance,
=QUERY(IMPORTRANGE(url,range),"select A where B matches 'Yes'") or similar
This imports data from a second spreadsheet and then the QUERY() function acts as a way of filtering the imported range by certain criteria.
Once the imported range is authorised, the editors of the spreadsheet can access it by, e.g. removing or modifying the query. You could prevent this by protecting that particular cell, if needed.