Google apps scripting, count streak - google-apps-script

Column A has a timestamp in ascending order. Column G has employee names. Each cell in Column L has either a 1, a 0, or is blank. I'm attempting to calculate the latest streak of 1's in Column L, per employee. My current attempts have involved a list of employee names with the following filter, "filter L:L where G:G = employee name and L:L is not blank". My thought was to nest this filter inside a custom formula that iterates through the filtered results, counting the streak of 1s and stopping at the first 0, returning the count of ones. Since the timestamps are in ascending order, I would need it to iterate from the last row up (or figure out how to change my data imports to append to the top of the sheet instead of the bottom). I have very little programming experience, but here was my failed attempt. How incredibly far off am I? (btw, I only just realized I need it to iterate from bottom up):
function streak() {
var sheet = SpreadsheetApp.getActiveSheet()
var range = sheet.getActiveRange();
var cell = sheet.getActiveCell();
var value = cell.getValue();
for (i in range) {
var count = 0
if (value = 1) {
count += 1;
} <br>
return count;
} <br>
} <br>

You are sort of close but depending on what you want to do, this could end up being quite complex. Nevertheless, you are on the right track.
What you want to do first is get the values for the active range on your spreadsheet.
var myDataVals = SpreadsheetApp.getActiveSheet().getActiveRange().getValues();
This will assign a 2-dimensional array to myDataVals.
From here you can loop through the outer array (row by row) and then the inner array within each (column by column). There are many ways to solve this question and it really depends on exactly what you're trying to do. You can learn more about basic JavaScript programming here: https://www.javascript.com/resources
I wrote a sample function that you can play around with. My solution was to iterate through the rows (outer array) and then assign them to an object where the keys are the employee names; this effectively sorts the rows by name. Then I sorted the rows within each name by timestamp value in descending order. Then, starting from the first (most recent) timestamp, I check to see if column L has a 1 or a 0. If I find a 1, I increment the number located in the streaks object at the key with the name by 1. If I find a 0, the streak is broken and I exit the while loop by changing my streakEnded boolean to true. If the cell is blank or the value is anything other than a 1 or 0, no action is taken and the loop proceeds until it is stopped or there are no more rows remaining.
Finally, the streaks object is returned. From there you could make a new page in the sheet, or send the results in an email, or anything else you may want to do. For now, I just logged the object to the script logger. You can see the results by choosing (View > Logs) in the script editor. Make sure you've highlighted the range of cells!
function streak() {
var activeRangeVals, allStreaks, columns;
// Get the active range as a 2-dimensional array of values
activeRangeVals = SpreadsheetApp.getActiveSheet().getActiveRange().getValues();
// Define which columns contain what data in the active Range
// There are better ways to do this (named ranges)
// but this was quickest for me.
columns = {
'name': 6, // G
'streak': 11, // L
'time': 0 // A
};
allStreaks = getStreaks(getEmployeeRowsObject(activeRangeVals));
Logger.log(allStreaks);
return allStreaks;
function getEmployeeRowsObject(data) {
// Create an empty object to hold employee data
var activeName, activeRow, employees = {}, i = 0;
// Iterate through the data by row and assign rows to employee object
// using employee names as the key
for(i = 0; i < data.length; i+= 1) {
// Assign the active row by copying from the array
activeRow = data[i].slice();
// Assign the active name based on info provided in the columns object
activeName = activeRow[columns['name']];
// If the employee is already defined on the object
if(employees[activeName] !== undefined) {
// Add the row to the stack
employees[activeName].push(activeRow);
// If not:
} else {
// Define a new array with the first row being the active row
employees[activeName] = [activeRow];
}
}
return employees;
}
function getStreaks(employees) {
var activeRow, activeStreak, i = 0, streaks = {}, streakEnded;
for(employee in employees) {
activeRow = employees[employee];
// Sort by timestamp in descending order (most recent first)
activeRow = activeRow.sort(function(a, b) {
return b[columns['time']].getTime() - a[columns['time']].getTime();
});
i = 0, streaks[employee] = 0, streakEnded = false;
while(i < activeRow.length && streakEnded === false) {
activeStreak = parseInt(activeRow[i][columns['streak']]);
if(activeStreak === 1) {
streaks[employee] += 1;
} else if(activeStreak === 0) {
streakEnded = true;
}
i += 1;
}
}
return streaks;
}
}

Related

google sheets custom averaging function

I have basic coding skills from college but not familiar with Apps Script for Google Sheets so I'm sure my code is all over the place and there is probably an easier way but I REALLY need some help before I pull all my hair out.
What I'm trying to do:
create a function that returns the TOTAL average of a strain, from a column of averages with all strains on the sourceSheet (has multiple rows of same strain) and put into coordinating row (1 row per strain) on strainSheet. The part I get lost at is trying to make the function keep adding the averages to get a total average when there are multiple lines of the same strain.
Here's the code I have:
function getAvgYield(x) {
//Average Strain Yield function
var strainName1 = x;
//Make Spreadsheets
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceSheet = ss.getSheetByName('Strain Yields');
//var strainSheet = ss.getSheetByName('Strains');
//get row number of strainName2 being used
var sourceCell = sourceSheet.getCurrentCell().getRow();
//input necessary data from source sheet
var strainName2 = sourceSheet.getRange(sourceCell, 1).getValue();
var avgPerPlant = sourceSheet.getRange(sourceCell, 5).getValue();
//create & define variables
var avgs = 0;
var avgYield = 0;
var tempYield = 0;
if (strainName1 == strainName2)
{
tempYield += avgPerPlant;
avgs++;
sourceCell++;
strainName2 = sourceSheet.getRange(sourceCell, 1).getValue();
} else
{
avgYield = tempYield / avgs;
avgs = 0;
tempYield = 0;
sourceCell++;
return avgYield;
}
avgYield = tempYield / avgs;
avgs = 0;
tempYield = 0;
return avgYield;
}
EDIT:
Here are screengrabs of the sheets. The info in "strain Yields" is being pulled from another document.
trying to fill "average yield" column
.
from the average yield column in this sheet - but adding same strains together.
.
hope this helps
Try this in cell C2 on the Strains tab.
=AVERAGEIF('Strain Yields'!A:A,A2,'Strain Yields'!E:E)
AVERAGEIF()
You could make a script that filled that for you, but it’s actually more flexible to have a function that computes and returns the result, so it can be used in different projects:
/**
* Compute the average for each group
*
* #param criteria_range Column of criterias to check the groups against (will be checked against group).
* #param group_range Column with the names of the group.
* #param average_range Column of values to be averaged if the criteria is matched with the group.
* #param no_elems Message to show if the group has no elements. By the fault it's "No elements".
*
* #returns the average of the values for each group
*/
function AVERAGEGROUPS(criteria_range, group_range, average_range, no_elems="No elements") {
// Input cleanup
criteria_range = criteria_range.map(row => row[0])
group_range = group_range.map(row => row[0])
average_range = average_range.map(row => row[0])
// Calculate all the averages
const nelements = Math.min(criteria_range.length, average_range.length)
return group_range.map(group => {
if (!group) return // early return if it doesn't have a group
let total = 0
let count = 0
for (let i = 0; i < nelements; i++) {
if (criteria_range[i] === group) {
total += average_range[i]
count++
}
}
if (count === 0) {
// Prevent dividing by 0
return no_elems
}
return total / count
})
}
Notice that this algorithm works only for columns. This makes the code way simpler and it’s your use case (and I think the most usual).
To use it, add the following to the “Average Yield” header:
={"Average Yield"; AVERAGEGROUPS('Strain Yields'!A2:A, A2:A, 'Strain Yields'!B2:B)}
Algorithm rundown
The first thing the function does is extract the values from the 2D array of values (array of rows). It does so by getting the first element of the row, so it ends up having a single column of values in an array.
range.map(row => row[0]) // Returns the values in the first column
Now it’s time for the average. nelements is just the length of the elements that we have to check. It’s computed as the minimum of length to prevent the code going out of range. Then we use map (MDN documentation) to make a 1:1 mapping between each group and the average. Inside it’s the actual average computation.
This is quite simple to do, we need to add the elements and the count if it’s part of the group and divide it at the end. We need to be careful of groups without elements. In that case we will return a string stating that (there is no way to return an error).
References
Array.prototype.map() (MDN)
Math.min() (MDN)
Average (Wikipedia)

How can I shift rows by one when they match a substring?

Could someone please help me solve this?
Attached is the example sheet of what I am trying to do:
https://docs.google.com/spreadsheets/d/12w4rGArGi1I1wlpm5yJJtT5AAlMM4LcZC31_DpP6jZQ/edit?usp=sharing
I am trying to shift the rows that contain "PO" in my data selection. (see shift function)
It should change from this:
to this:
I have written a script but it isn't working and I am not getting an error message.
I have a feeling it is because I am trying to "+1" my array to offset my values. Please help!
Here is my current script:
function shift() {
try{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var as = ss.getActiveSheet();
var ar = as.getActiveRange();
var vals = ar.getValues(); // get values from the active (selected) range...... intended use is to draw a selection where the PO BOX substring is in the leftmost column
// SpreadsheetApp.getUi().alert(vals); //for checking values
var r; // variable for rows
for (r = 0; r < vals.length; r++){ // for each row, up to the last row (iterate over all rows from top to bottom)
if(vals[r][0].indexOf("PO") != -1){ // if first column in each row contains "PO"
SpreadsheetApp.getUi().alert("found a PO BOX"); // make annoucement
var c; // variable for columns
var cols = []; // array for column data (for debugging)
for (c = 0; c < vals[r].length; c++){ // for each columns in row (iterating over each cell in row from left to right)
cols[c] = vals[r][c]; // add the current cell value to an array
vals[r][c+1] = vals[r][c]; // take the value from the current cell and assign it to the next cell (+1 to the column)
}
SpreadsheetApp.getUi().alert(cols); // show me the data that cas changed
}
}
ar.setValues(vals); // set new values to active range
}
catch(err){
SpreadsheetApp.getUi().alert(err);
}
}
Expectations:
get the data range (my intended testing range is B:1 to D:12)
iterate through each row, and on each row, iterate through each
cell(column)
If the very first cell in the current row(vals[r][0]) contains the
substring "PO" , then I want to change the values of that row such
that they all shift over by one column, and leave the very first
cell as a blank string
I change the values by replacing the current values with the same
values BUT +1 to the columns row(vals[r][c]) = row(vals[r][c+1])
Realit(EDIT)y:
Exceeded memory limit error... possible infinite loop happening... i THINK it might be because I am adding a column that does not exist in the data range, but how would I get around this problem?
SOLVED!!!!
After much trial and error, I cane up with an answer (posted int he answers)
feels great to solve my own problem for the first time ever!
my problem was exactly what i expected. I was trying to assign values outside of the range.
The way that I solved this was to assign all of my values to an array(I also shifted them over in the process), and then loop through the range(the range is just a two dimensional array), assigning all of the shifted values to the range columns.
function shift() {
try{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var as = ss.getActiveSheet();
var ar = as.getActiveRange();
var vals = ar.getValues();
var r; //variable for rows
for (r = 0; r < vals.length; r++){ // for each row, up to the last row (iterate over all rows from top to bottom)
if((vals[r][0].indexOf("PO") != -1)||(vals[r][0].indexOf("P0") != -1)){ // if first column in each row contains "PO"
var c; // variable for columns
var cols = []; // array to store all data temporarily (will be uses to set new values later)
for (c = 0; c < vals[r].length; c++){ // then iterate over each column(cell) in the row
if(c == 0){ // if it is the first row,
cols[c+1] = vals[r][c]; // assign second index of the array with the PO value (to simulate a shift)
cols[c] = ""; // assign the first index of the array a blank string
}
else{ // if it is not the first row
cols[c+1] = vals[r][c]; // assign each additional column value to the next index (+1) of the array
}
}
for (c = 0; c < vals[r].length; c++){ // once the array is finished, loop through the columns again foreach row
vals[r][c] = cols[c]; // this time, assigning the new values to the corresponding array indices
}
}
}
ar.setValues(vals); // now, set the values that you reassinged to the array
}
catch(err){
SpreadsheetApp.getUi().alert(err);
}
}

Insert row between different data

I'm Andrew from Italy!
I'm trying to write a script that inserts a blank row between some data in a spreadsheet.
Example, I have this data set:
and I want to obtain something like this:
In other words:
The script should check all the rows and, when the date change and the name change, must add an empty row.
Any idea about this?
I've tried to modify a script that I've found online that remove blank rows in a sheet but I'm not able to get any results.
Thanks!
(: I agree with Sandy Good... Just of the top of my head, here's a piece of code that can get you started:
function doSomething() {
var spreadsheet = SpreadsheetApp.openById('yourID'), // Get the spreadsheet
tab = spreadsheet.getSheets()[0], // Get the first tab
values = tab.getRange(2, 1, tab.getLastRow(), 3).getDisplayValues(), //Get the values beginning in the second row because of the headers
newValues = [], // Empty array where we're gonna push the new values
lastDate,
lastName;
// Loop through all the values
for(var i = 0; i <values.length; i++){
// If the last name is different from the current name or the last date is different from the current date add an empty "row" to the new array
if((lastDate != undefined && lastName != undefined) && (values[i][0] != lastDate || values[i][1] != lastName)){
newValues.push(['','','']);
}
//Add the current row to the new array
newValues.push(values[i]);
//Sets the lastDate and lastName to the current values for the next evaluation
lastDate = values[i][0];
lastName = values[i][1];
}
//Sets the new values
tab.getRange(2,1,newValues.length,3).setValues(newValues)
}

Spreadsheet formula taking too long to run - script advise

The following formula is taking far to long to run.
= TRANSPOSE (
IFERROR (
INDEX (
FILTER(Students!B:B;
REGEXMATCH(Students!B:B; C50),
REGEXMATCH(Students!B:B; B50),
REGEXMATCH(Students!C:C; F50)
));"NO MATCH"
))
Any suggestions on the coding would be great, as I know very little programming.
Thanks
T
Here is a custom function that can replace the formula you're using. For example:
=listStudents(C50,B50,F50)
If used that way, you'll still have constant recalculation, but it should be much faster than the regex tests. Alternatively, the same function could be invoked from a menu item, and used to populate a given target range in the sheet, thereby avoiding automatic recalculation altogether.
Code:
/**
* Custom spreadsheet function to produce a list of names of
* students that match the given criteria.
*/
function listStudents( givenName, surname, employer ) {
var matches = []; // matching students will be placed in this array
var HEADERS = 1; // # rows of header info at top of sheet
var FULLNAME = 1; // Column containing full names (B)
var EMPLOYER = 2; // employers (C)
// Array filter function - returns true if conditions match
function test4match( row ) {
return ( row[FULLNAME].indexOf(givenName) !== -1 &&
row[FULLNAME].indexOf(surname) !== -1 &&
row[EMPLOYER].indexOf(employer) !== -1)
}
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Students');
var range = sheet.getDataRange();
var data = range.getValues().slice(HEADERS); // All data from sheet, without headers
var filteredData = data.filter(test4match); // Get matching rows
for (var i=0; i<filteredData.length; i++) {
matches.push(filteredData[i][FULLNAME]); // Then produce list of names
}
return [matches]; // Return a 2-d array, one row
}

Improve spreadsheet filtering script performance

I have a spreadsheet where first column contains names and next 17 contains 0, 1 or are empty. Every row looks like this:
foobar 0 0 0 1 0 1 // and so on
I need to make function, called from menu, that shows the user only rows with 1 in the target column (arg1). Here is code:
var ssBase = SpreadsheetApp.getActiveSheet();
var last = ssBase.getLastRow() ;
var data = ssBase.getDataRange().getValues();
function SkillsFilter(arg1){
ssBase.showRows(1, last+1);
for (var i=1; i < last; i++){
if (data[i][arg1] != "1"){
ssBase.hideRows(i+1);
}}}
This function doesn't perform as fast as I'd like. How should I increase performance? Will cache help me or something else?
You're making many calls to the Spreadsheet services within your for loop - if you can change those many operations into one, you'll see a great speed improvement. Read Best Practices for some background and guidance.
I suggest that you reconsider the approach of hiding & showing various rows of data. Instead, you could display a filtered list, and manipulate that filter using your menu functions. Let's say the data you have looks like this...
On a second tab in the spreadsheet, you could provide the filtered version of your list. The following formula in cell A2 would create a filtered list of data from the orignal data sheet (called "Master" in this example):
=filter(Master!A2:R;Master!D2:D="1")
To create that filter programmatically, use the setFormulaR1C1() function. Here is a function that you could call from your menu items to set the filter for any particular column.
/**
* Sets the filter in cell A2 of the sheet named "Filter" to display
* only rows with a number 1 in the indicated column.
*
* #param {number} column The "skill" column to filter for
*/
function setFilter(column) {
column = column | 2; // Use first "skill" column as default
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Filter");
var formula = "Master!R[0]C[0]:C[17];Master!R[0]C["
+column
+"]:C["
+column
+"]=1";
sheet.getRange("A2").setFormulaR1C1(formula);
}
This piece of code will show all rows that contain the number 1, thought the column range:
function mySort() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
// set col and row
var col = sheet.getLastColumn(), row = sheet.getLastRow()-1;
// hide all rows
sheet.hideRows(2, row);
// itterate through columns
for(var k=0; k<col; k++) {
this.data = sheet.getRange(2, 1, row, col)
.sort({column: parseInt(k+1), ascending: true}).getValues();
//set counters
var cFirst=0, cSecond=0;
// itterate to find number of "1" rows in column k
for(var i=0; i<row; i++) {
if(this.data[i][k] == 1) {
cFirst++;
} else {
cSecond++;
}
}
// calculate rowIndex
var rIndex = row-cSecond;
// show (unhide) only "1" rows
sheet.showRows(rIndex+2, cFirst);
}
}
See example file I created: Show the one's. When the file is opened, it will add another menu item called Sorting. There you can play around and see what happens.