Google sheets script multiple range and multiple conditional - google-sheets

I have a spreadsheet, in which I need the script to fetch, over two columns, two pieces of information. And when that's true, I add a formula that adds "1 day" into a third column.
I can make the conditionals run individually. But when I put both, they don't work.
function fillColADia() {
var s = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Afazeres');
var dataA = s.getRange("H1:H").getValues();
var dataB = s.getRange("A1:A").getValues();
for(var i=0; i<dataA.length; i++) {
for(var j=0; j<dataB.lenght; j++) {
if (dataB[j][0] == false) {
if (dataA[i][0] == 'Zenon') {
s.getRange(i+1,3).setValue("+1");
}
}
}
}
}
And I also don't know how to add the "add 1 day" formula to the end.
Thanks a lot for the help.
COPY FILE with dummy data

Faaala! Something along these lines as food for thought!
...although I'm certain there are more performant ways to accomplish it:
function fillColADia() {
var s = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Page1');
var data = s.getRange("A1:H").getValues();
for (var a = 0; a < data.length; a++) {//Percorre cada linha do intervalo
if (data[a][0] == false && data[a][7] == 'Zenon') {//Aplica critérios
let date = data[a][2];//Pega a data daquela linha
date1 = Utilities.formatDate(addDays(new Date(date), 1), Session.getTimeZone(), "dd/MM/yyyy");//Formata o objeto data, usando a função abaixo para incrementar o dia
date2 = Utilities.formatDate(addDays(new Date(date), 1), Session.getTimeZone(), "dd/MM/yyyy HH:mmss");
s.getRange(a + 1, 3).setValue(date1);//Põe a data de volta
s.getRange(a + 1, 4).setValue(date2);//Põe a data de volta
}
}
}
//GERA DATAS INCREMENTANDO OS DIAS
function addDays(date, days) {
var result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}

Related

Google sheet get fasten array for loop

Is it possible to fasten the hide of many rows when range.length > 300 rows ?
I also can't succeed moving the focus to the top of the sheet once the rows are hidden, I can only get the focus on another sheet.
Here is my code (french parameters), I'm not sure I need to show my spreadsheet. Thank you very much.
var LastRow = sheet.getLastRow()-1;
var ToHide = [];
for (var i=1 ; i < LastRow +1 ; i++){
if ( sheet.getRange(i,1).isChecked() == null){ ToHide.push(i); }
}
for (var j=0 ; j<ToHide.length ; j++){ MyActiveSheet.hideRows(ToHide[j+1],1); }
ToHide.forEach(function (d){ FeuilleActive.hideRows(d); }); // also tried .hideRows(d,1)
SpreadsheetApp.getActive().getSheets()[5].getRange(1,1).activate(); //
To lessen the number of loops, you can ​group it by determining the series of consecutive numbers in your Array. Which you can use to determine the index and number of rows to hide. The number of .hideRows() execution will be determined by the number of series in your array. Thus, lesser runtime.
Example Code:
function myFunction() {
var a = [1,2,3,4,6,7,8,9,11,13,14];
const result = a.reduce((r, n) => {
const lastSubArray = r[r.length - 1];
if(!lastSubArray || lastSubArray[lastSubArray.length - 1] !== n - 1) {
r.push([]);
}
r[r.length - 1].push(n);
return r;
}, []);
//result output: [[1.0, 2.0, 3.0, 4.0], [6.0, 7.0, 8.0, 9.0], [11.0], [13.0, 14.0]]
result.forEach(e => {
var index = e[0];
var numRows = e.length;
Logger.log("Index: "+ index);
Logger.log("numRows: "+numRows);
// MyActiveSheet.hideRows(index,numRows);
})
}
Output:
References:
Ori Drori answer on how to group series of consecutive numbers in an Array.
hideRows(rowIndex, numRows)

Joining tables in queries google sheets

Basically I have two google sheets that look something like this:
a table where people can put in their email and select what kind of foo they are using
email
foo
example#email.com
This Foo
and then another table with information about the foo
foo name
foo type
foo boolean1
foo boolean2
This Foo
String
True
True
That Foo
Number
False
True
Other Foo
String
False
False
In a Separate Sheet I'd like to have a dashboard-like view of things wherein I would have counts of various things like number of people, how many of each type of Foo, etc
Where I'm having trouble is figuring out how to pull things like "Number of people who have selected String foos" and such
like, basically i want the google-query equivalent to (in sql)
SELECT COUNT(p.*) FROM people p JOIN info i on p.foo = i.foo_name GROUP BY i.foo_type WHERE i.foo_type = 'String'
What I would be looking for is a table that looks like this:
Data
Count
Active Roster
4
String
3
Number
1
I have also seen many solutions that have complicated formulas using VLOOKUP, INDEX, MATCH, etc.
I decided to write a user function to combine tables, or as I refer to it, de-normalize the database. I wrote the function DENORMALIZE() to support INNER, LEFT, RIGHT and FULL joins. By nesting function calls one can join unlimited tables in theory.
DENORMALIZE(range1, range2, primaryKey, foreignKey, [joinType])
Parameters:
range1, the main table as a named range, a1Notation or an array
range2, the related table as a named range, a1Notation or an array
primaryKey, the unique identifier for the main table, columns start with "1"
foreignKey, the key in the related table to join to the main table, columns start with "1"
joinType, type of join, "Inner", "Left", "Right", "Full", optional and defaults to "Inner", case insensitive
Returns: results as a two dimensional array
Result Set Example:
=QUERY(denormalize("Employees","Orders",1,3), "SELECT * WHERE Col2 = 'Davolio' AND Col8=2", FALSE)
EmpID
LastName
FirstName
OrderID
CustomerID
EmpID
OrderDate
ShipperID
1
Davolio
Nancy
10285
63
1
8/20/1996
2
1
Davolio
Nancy
10292
81
1
8/28/1996
2
1
Davolio
Nancy
10304
80
1
9/12/1996
2
Other Examples:
=denormalize("Employees","Orders",1,3)
=denormalize("Employees","Orders",1,3,"full")
=QUERY(denormalize("Employees","Orders",1,3,"left"), "SELECT * ", FALSE)
=QUERY(denormalize("Employees","Orders",1,3), "SELECT * WHERE Col2 = 'Davolio'", FALSE)
=QUERY(denormalize("Employees","Orders",1,3), "SELECT * WHERE Col2 = 'Davolio' AND Col8=2", FALSE)
=denormalize("Orders","OrderDetails",1,2)
// multiple joins
=denormalize("Employees",denormalize("Orders","OrderDetails",1,2),1,3)
=QUERY(denormalize("Employees",denormalize("Orders","OrderDetails",1,2),1,3), "SELECT *", FALSE)
=denormalize(denormalize("Employees","Orders",1,3),"OrderDetails",1,2)
=QUERY(denormalize("Employees",denormalize("Orders","OrderDetails",1,2),1,3), "SELECT *", FALSE)
=QUERY(denormalize(denormalize("Employees","Orders",1,3),"OrderDetails",4,2), "SELECT *", FALSE)
function denormalize(range1, range2, primaryKey, foreignKey, joinType) {
var i = 0;
var j = 0;
var index = -1;
var lFound = false;
var aDenorm = [];
var hashtable = [];
var aRange1 = "";
var aRange2 = "";
joinType = DefaultTo(joinType, "INNER").toUpperCase();
// the 6 lines below are used for debugging
//range1 = "Employees";
//range1 = "Employees!A2:C12";
//range2 = "Orders";
//primaryKey = 1;
//foreignKey = 3;
//joinType = "LEFT";
// Sheets starts numbering columns starting with "1", arrays are zero-based
primaryKey -= 1;
foreignKey -= 1;
// check if range is not an array
if (typeof range1 !== 'object') {
// Determine if range is a1Notation and load data into an array
if (range1.indexOf(":") !== -1) {
aRange1 = ss.getRange(range1).getValues();
} else {
aRange1 = ss.getRangeByName(range1).getValues();
}
} else {
aRange1 = range1;
}
if (typeof range2 !== 'object') {
if (range2.indexOf(":") !== -1) {
aRange2 = ss.getRange(range2).getValues();
} else {
aRange2 = ss.getRangeByName(range2).getValues();
}
} else {
aRange2 = range2;
}
// make similar structured temp arrays with NULL elements
var tArray1 = MakeArray(aRange1[0].length);
var tArray2 = MakeArray(aRange2[0].length);
var lenRange1 = aRange1.length;
var lenRange2 = aRange2.length;
hashtable = getHT(aRange1, lenRange1, primaryKey);
for(i = 0; i < lenRange2; i++) {
index = hashtable.indexOf(aRange2[i][foreignKey]);
if (index !== -1) {
aDenorm.push(aRange1[index].concat(aRange2[i]));
}
}
// add left and full no matches
if (joinType == "LEFT" || joinType == "FULL") {
for(i = 0; i < lenRange1; i++) {
//index = aDenorm.indexOf(aRange1[i][primaryKey]);
index = aScan(aDenorm, aRange1[i][primaryKey], primaryKey)
if (index == -1) {
aDenorm.push(aRange1[i].concat(tArray2));
}
}
}
// add right and full no matches
if (joinType == "RIGHT" || joinType == "FULL") {
for(i = 0; i < lenRange2; i++) {
index = aScan(aDenorm, aRange2[i][foreignKey], primaryKey)
if (index == -1) {
aDenorm.push(tArray1.concat(aRange2[i]));
}
}
}
return aDenorm;
}
function getHT(aRange, lenRange, key){
var aHashtable = [];
var i = 0;
for (i=0; i < lenRange; i++ ) {
//aHashtable.push([aRange[i][key], i]);
aHashtable.push(aRange[i][key]);
}
return aHashtable;
}
function MakeArray(length) {
var i = 0;
var retArray = [];
for (i=0; i < length; i++) {
retArray.push("");
}
return retArray;
}
function DefaultTo(valueToCheck, valueToDefault) {
return typeof valueToCheck === "undefined" ? valueToDefault : valueToCheck;
}
// Search a multi-dimensional array for a value
function aScan(aValues, searchStr, searchCol) {
var retval = -1;
var i = 0;
var aLen = aValues.length;
for (i = 0; i < aLen; i++) {
if (aValues[i][searchCol] == searchStr) {
retval = i;
break;
}
}
return retval;
}
You can make a copy of the google sheet with data and examples here:
https://docs.google.com/spreadsheets/d/1vziuF8gQcsOxTLEtlcU2cgTAYL1eIaaMTAoIrAS7mnE/edit?usp=sharing

Automatic hiding columns based on cell value in google spreadsheets

I have too many columns in the sheet and so many columns to hide as well, but while executing the script it runs half way and stopped saying maximum time reached.
When I again tried to execute it stopped exactly where I stopped previously. So I would like to have some customization that if the column is already hidden can skip that column and work on the others.
Is there any way to do it.
Here is the code I used:
function hideColumns() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('ANALYTIC');
var data = sh.getRange('6:6').getValues();
var numCols = sh.getMaxColumns();
var numRows = sh.getMaxRows();
for(var i = 0; i <= numCols; i++){
if(data[0][i] == ""){
sh.hideColumns(i+1);
} else {
sh.unhideColumn(sh.getRange(1, i+1, numRows, 1));
}
}
}
Please help me.
You can use the documentProperties to store the last column before the end of the execution. To prevent the run from stopping abruptly you stop the run a little prematurely at 5min (execution will terminate at 6min mark) mark and store the column number in the documentProperty. You also display an alert asking you rerun the script.
Then retrieve the column number on the next run and start from there. If the program gets through the complete loop you delete the said properties. So you start from zero if you rerun the script next time.
Below is the code for the same
function hideColumns() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('ANALYTIC');
var data = sh.getRange('6:6').getValues();
var numCols = sh.getMaxColumns();
var numRows = sh.getMaxRows();
var docProp = PropertiesService.getDocumentProperties()
var startCol = Number(docProp.getProperty("startCol")) //if there is no propert called startCol will return Null, Number(Null) = 0
Logger.log(startCol)
var startTime = new Date().getTime()
var ms5min = 5*60*1000 //5min in millseconds
for(var i = startCol; i <= numCols; i++){
if(data[0][i] == ""){
sh.hideColumns(i+1);
} else {
sh.unhideColumn(sh.getRange(1, i+1, numRows, 1));
}
var curTime = new Date().getTime()
var elasped = curTime-startTime
if (elasped >= ms5min){
docProp.setProperty("startCol", i)
SpreadsheetApp.getUi().alert("Please restart Run, exceeded 5min mark")
return
}
}
Logger.log(elasped)
docProp.deleteAllProperties()
}
If your sheet has under 10,000 columns (PropertiesService limit) you can use this script:
function hideColumns() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('ANALYTIC');
var data = sh.getRange('6:6').getValues();
var documentProperties = PropertiesService.getDocumentProperties()
var documentPropertiesVals = documentProperties.getProperties();
var numCols = sh.getMaxColumns();
for(var i = 0; i < numCols; i++){
if (!(cPlusInt(i) in documentPropertiesVals)) {
documentPropertiesVals[cPlusInt(i)] === 'empty';
}
if (documentPropertiesVals[cPlusInt(i)] === 'hidden' && data[0][i] == "") continue;
if (documentPropertiesVals[cPlusInt(i)] === 'unhidden' && data[0][i] != "") continue;
if(data[0][i] == ""){
sh.hideColumns(i+1);
documentProperties.setProperty(cPlusInt(i), 'hidden')
} else {
sh.unhideColumn(sh.getRange(1, i+1, 1, 1));
documentProperties.setProperty(cPlusInt(i), 'unhidden')
}
}
}
function cPlusInt(num) {
return 'c'+num.toString()
}
You at first you may need to run this few times (many write operation to PropertiesService may be sluggish) but later it's "blazingly" fast (0.1 s per new column).
Better answer using #Jack Brown's idea
It is possible to make single save to PropertiesService - you would need to incorporate time limit from #Jack Brown's answer, this way:
function hideColumns() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('ANALYTIC');
var data = sh.getRange('6:6').getValues();
var documentProperties = PropertiesService.getDocumentProperties()
var documentPropertiesVals = documentProperties.getProperties();
var numCols = sh.getMaxColumns();
var startTime = new Date().getTime()
var ms5min = 5*60*1000 //5min in millseconds
for(var i = 0; i < numCols; i++){
if (!(cPlusInt(i) in documentPropertiesVals)) {
documentPropertiesVals[cPlusInt(i)] === 'empty';
}
if (documentPropertiesVals[cPlusInt(i)] === 'hidden' && data[0][i] == "") continue;
if (documentPropertiesVals[cPlusInt(i)] === 'unhidden' && data[0][i] != "") continue;
if(data[0][i] == ""){
sh.hideColumns(i+1);
documentPropertiesVals[cPlusInt(i)] = 'hidden'
} else {
sh.unhideColumn(sh.getRange(1, i+1, 1, 1));
documentPropertiesVals[cPlusInt(i)] = 'unhidden'
}
var curTime = new Date().getTime()
var elapsed = curTime-startTime
if (elapsed >= ms5min){
break;
}
}
documentProperties.setProperties(documentPropertiesVals)
if (elapsed >= ms5min){
SpreadsheetApp.getUi().alert("Please restart Run, exceeded 5min mark")
}
}
cPlusInt function explanation
cPlusInt is necessary because of weird problems with google's PropertiesService. Object gotten from PropertiesService returns undefined at integer keys. To see problem use this code:
function test() {
var obj = {};
for (var i=0; i<10; i++) {
obj[i.toString()] = 'aa' + i.toString();
}
var documentProperties = PropertiesService.getScriptProperties();
documentProperties.deleteAllProperties();
documentProperties.setProperties(obj);
obj = documentProperties.getProperties();
Logger.log(obj)
for (var i in obj) {
Logger.log('%s %s %s', i, obj[i], i === '1');
}
}

How can I properly parse an email address with name?

I'm reading email headers (in Node.js, for those keeping score) and they are VARY varied. E-mail addresses in the to field look like:
"Jake Smart" <jake#smart.com>, jack#smart.com, "Development, Business" <bizdev#smart.com>
and a variety of other formats. Is there any way to parse all of this out?
Here's my first stab:
Run a split() on - to break up the different people into an array
For each item, see if there's a < or ".
If there's a <, then parse out the email
If there's a ", then parse out the name
For the name, if there's a ,, then split to get Last, First names.
If I first do a split on the ,, then the Development, Business will cause a split error. Spaces are also inconsistent. Plus, there may be more e-mail address formats that come through in headers that I haven't seen before. Is there any way (or maybe an awesome Node.js library) that will do all of this for me?
There's a npm module for this - mimelib (or mimelib-noiconv if you are on windows or don't want to compile node-iconv)
npm install mimelib-noiconv
And the usage would be:
var mimelib = require("mimelib-noiconv");
var addressStr = 'jack#smart.com, "Development, Business" <bizdev#smart.com>';
var addresses = mimelib.parseAddresses(addressStr);
console.log(addresses);
// [{ address: 'jack#smart.com', name: '' },
// { address: 'bizdev#smart.com', name: 'Development, Business' }]
The actual formatting for that is pretty complicated, but here is a regex that works. I can't promise it always will work though. https://www.rfc-editor.org/rfc/rfc2822#page-15
const str = "...";
const pat = /(?:"([^"]+)")? ?<?(.*?#[^>,]+)>?,? ?/g;
let m;
while (m = pat.exec(str)) {
const name = m[1];
const mail = m[2];
// Do whatever you need.
}
I'd try and do it all in one iteration (performance). Just threw it together (limited testing):
var header = "\"Jake Smart\" <jake#smart.com>, jack#smart.com, \"Development, Business\" <bizdev#smart.com>";
alert (header);
var info = [];
var current = [];
var state = -1;
var temp = "";
for (var i = 0; i < header.length + 1; i++) {
var c = header[i];
if (state == 0) {
if (c == "\"") {
current.push(temp);
temp = "";
state = -1;
} else {
temp += c;
}
} else if (state == 1) {
if (c == ">") {
current.push(temp);
info.push (current);
current = [];
temp = "";
state = -1;
} else {
temp += c;
}
} else {
if (c == "<"){
state = 1;
} else if (c == "\"") {
state = 0;
}
}
}
alert ("INFO: \n" + info);
For something complete, you should port this to JS: http://cpansearch.perl.org/src/RJBS/Email-Address-1.895/lib/Email/Address.pm
It gives you all the parts you need. The tricky bit is just the set of regexps at the start.

Hash of a cell text in Google Spreadsheet

How can I compute a MD5 or SHA1 hash of text in a specific cell and set it to another cell in Google Spreadsheet?
Is there a formula like =ComputeMD5(A1) or =ComputeSHA1(A1)?
Or is it possible to write custom formula for this? How?
Open Tools > Script Editor then paste the following code:
function MD5 (input) {
var rawHash = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, input);
var txtHash = '';
for (i = 0; i < rawHash.length; i++) {
var hashVal = rawHash[i];
if (hashVal < 0) {
hashVal += 256;
}
if (hashVal.toString(16).length == 1) {
txtHash += '0';
}
txtHash += hashVal.toString(16);
}
return txtHash;
}
Save the script after that and then use the MD5() function in your spreadsheet while referencing a cell.
This script is based on Utilities.computeDigest() function.
Thanks to gabhubert for the code.
This is the SHA1 version of that code (very simple change)
function GetSHA1(input) {
var rawHash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_1, input);
var txtHash = '';
for (j = 0; j <rawHash.length; j++) {
var hashVal = rawHash[j];
if (hashVal < 0)
hashVal += 256;
if (hashVal.toString(16).length == 1)
txtHash += "0";
txtHash += hashVal.toString(16);
}
return txtHash;
}
Ok, got it,
Need to create custom function as explained in
http://code.google.com/googleapps/appsscript/articles/custom_function.html
And then use the apis as explained in
http://code.google.com/googleapps/appsscript/service_utilities.html
I need to handtype the complete function name so that I can see the result in the cell.
Following is the sample of the code that gave base 64 encoded hash of the text
function getBase64EncodedMD5(text)
{
return Utilities.base64Encode( Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, text));
}
The difference between this solution and the others is:
It fixes an issue some of the above solution have with offsetting the output of Utilities.computeDigest (it offsets by 128 instead of 256)
It fixes an issue that causes some other solutions to produce the same hash for different inputs by calling JSON.stringify() on input before passing it to Utilities.computeDigest()
function MD5(input) {
var result = "";
var byteArray = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, JSON.stringify(input));
for (i=0; i < byteArray.length; i++) {
result += (byteArray[i] + 128).toString(16) + "-";
}
result = result.substring(result, result.length - 1); // remove trailing dash
return result;
}
to get hashes for a range of cells, add this next to gabhubert's function:
function RangeGetMD5Hash(input) {
if (input.map) { // Test whether input is an array.
return input.map(GetMD5Hash); // Recurse over array if so.
} else {
return GetMD5Hash(input)
}
}
and use it in cell this way:
=RangeGetMD5Hash(A5:X25)
It returns range of same dimensions as source one, values will spread down and right from cell with formulae.
It's universal single-value-function to range-func conversion method (ref), and it's way faster than separate formuleas for each cell; in this form, it also works for single cell, so maybe it's worth to rewrite source function this way.
Based on #gabhubert but using array operations to get the hexadecimal representation
function sha(str){
return Utilities
.computeDigest(Utilities.DigestAlgorithm.SHA_1, str) // string to digested array of integers
.map(function(val) {return val<0? val+256 : val}) // correct the offset
.map(function(val) {return ("00" + val.toString(16)).slice(-2)}) // add padding and enconde
.join(''); // join in a single string
}
Using #gabhubert answer, you could do this, if you want to get the results from a whole row. From the script editor.
function GetMD5Hash(value) {
var rawHash = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, value);
var txtHash = '';
for (j = 0; j <rawHash.length; j++) {
var hashVal = rawHash[j];
if (hashVal < 0)
hashVal += 256;
if (hashVal.toString(16).length == 1)
txtHash += "0";
txtHash += hashVal.toString(16);
}
return txtHash;
}
function straightToText() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var r = 1;
var n_rows = 9999;
var n_cols = 1;
var column = 1;
var sheet = ss[0].getRange(r, column, n_rows, ncols).getValues(); // get first sheet, a1:a9999
var results = [];
for (var i = 0; i < sheet.length; i++) {
var hashmd5= GetMD5Hash(sheet[i][0]);
results.push(hashmd5);
}
var dest_col = 3;
for (var j = 0; j < results.length; j++) {
var row = j+1;
ss[0].getRange(row, dest_col).setValue(results[j]); // write output to c1:c9999 as text
}
}
And then, from the Run menu, just run the function straightToText() so you can get your result, and elude the too many calls to a function error.
I was looking for an option that would provide a shorter result. What do you think about this? It only returns 4 characters. The unfortunate part is that it uses i's and o's which can be confused for L's and 0's respectively; with the right font and in caps it wouldn't matter much.
function getShortMD5Hash(input) {
var rawHash = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, input);
var txtHash = '';
for (j = 0; j < 16; j += 8) {
hashVal = (rawHash[j] + rawHash[j+1] + rawHash[j+2] + rawHash[j+3]) ^ (rawHash[j+4] + rawHash[j+5] + rawHash[j+6] + rawHash[j+7])
if (hashVal < 0)
hashVal += 1024;
if (hashVal.toString(36).length == 1)
txtHash += "0";
txtHash += hashVal.toString(36);
}
return txtHash.toUpperCase();
}
I needed to get a hash across a range of cells, so I run it like this:
function RangeSHA256(input)
{
return Array.isArray(input) ?
input.map(row => row.map(cell => SHA256(cell))) :
SHA256(input);
}

Resources