Speed Up Find-and-Replace Google Apps Script Function for sheets - algorithm

I've written a pretty simple script that successfully takes information from one sheet in a Google Spreadsheet, and replaces information in a column in another sheet in the same spreadsheet pending satisfaction of two criteria: the receiving row has the same "Customer ID" and "Product Type." I say "simple" because it's intuitive, but extremely computationally demanding (taking nearly 30 seconds to run!).
From what I've read online, it's the sequential read and write operations that are causing the slowdown. I'm assuming that if I sort the sheets in question on the two criteria and THEN do a function that writes over subsequent rows, I may be able to speed it up. I'm a little weak on algorithms, so I'm still scratching my head on how to do this elegantly.
Does anyone have any suggestions? Below is my original script, and I've already made sure that the spreadsheet collapses empty rows, so time isn't wasted iterating over nothing.
function replaceRawWithRepChanges(receivedSheet) {
var ss = SpreadsheetApp.openById(receivedSheet);
var repchanges = ss.getSheetByName('repchanges');
var rawSheet = ss.getSheetByName('Sheet1');
var rawTMtoReplace = rawSheet.getRange('P2:P');
var repCustID = repchanges.getRange('A1:A').getValues();
var repTM = repchanges.getRange('F1:F').getValues();
var repCategory = repchanges.getRange('G1:G').getValues();
var rawCustID = rawSheet.getRange('A2:A').getValues();
var rawTM = rawSheet.getRange('P2:P').getValues();
var rawCategory = rawSheet.getRange('U2:U').getValues();
var repInfo = [repCustID, repTM, repCategory];
var rawInfo = [rawCustID, rawTM, rawCategory];
for (var i=0; i < rawInfo[0].length; i++) {
for (var j=0; j < repInfo[0].length; j++) {
// var thisRawCust = rawInfo[0][i];
// var thisRepCust = repInfo[0][j];
if (rawInfo[0][i].toString() == repInfo[0][j].toString()) {
// var thisRawCategory = rawInfo[2][i];
// var thisRepCategory = repInfo[2][j];
if (rawInfo[2][i].toString() == repInfo[2][j].toString()) {
// var repvalue = repInfo[1][j];
rawInfo[1][i] = repInfo[1][j];
// var newRawValue = rawInfo[1][i];
}
}
}
}
return rawInfo[1];
}

Yes, you should sort the data (perhaps using the SORT command, which does work with multiple columns). Then, using two pointers, you only have to go down the columns once, rather than checking the entirety of repInfo for matches for every single row in rawInfo.
Once you've sorted the information, your loop might look like the following:
var i = 0;
var j = 0;
while (i < rawInfo[0].length && j < repInfo[0].length) {
if (rawInfo[0][i].toString() == repInfo[0][j].toString()) {
if (rawInfo[2][i].toString() == repInfo[2][j].toString()) {
rawInfo[1][i]=repInfo[1][j];
i++;
j++;
} else if (rawInfo[2][i].toString() < repInfo[2][j].toString()) {
i++;
} else {
j++;
}
} else if (rawInfo[0][i].toString() < repInfo[0][j].toString()) {
i++;
} else {
j++;
}
}

Related

Filter & Delete rows of data based off of column value fast! (Google Sheets)

Is there a way to filter the data in column Q off my google sheet faster then reading line one by one. There is daily about 400+ lines it needs to scan through and I need to delete every row of data if the data in column Q is less than 1 right now watching it, it takes about 10+ minutes.
function UpdateLog() {
var returnSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('CancelRawData');
var rowCount = returnSheet.getLastRow();
for (i = rowCount; i > 0; i--) {
var rrCell = 'Q' + i;
var cell = returnSheet.getRange(rrCell).getValue();
if (cell < 1 ){
returnSheet.deleteRow(i);
}
}
{
SpreadsheetApp.getUi().alert("🎉 Congratulations, your data has been updated", SpreadsheetApp.getUi().ButtonSet.OK);
}
}
Try it this way:
function UpdateLog() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Sheet0');
const vs = sh.getDataRange().getValues();
let d = 0;
vs.forEach((r, i) => {
if (!isNaN(r[16]) && r[16] < 1){
sh.deleteRow(i + 1 - d++);
}
});
}
This is a bit quicker
function UpdateLog() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Sheet0');
const vs = sh.getDataRange().getValues().filter(r => !isNaN(r[16]) && r[16] < 1);
sh.clearContents();
sh.getRange(1,1,vs.length,vs[0].length).setValues(vs);
}

How to copy a formula from "Parent Tab" to "Child Tab" on Google Sheet

I'm doing RolePlay Character Sheets on a "Parent tab" I've called "MODEL", where I masterize my formulas.
I've created a second tab "Character1" and a third one "Character2". But when I try to use =QUERY or =TEXTFORMULA or whatever. It doesn't make the formulas to calculate on the actual spreadsheet, it just get the data from the "MODEL" tab.
My only way is actually to copy/past all my formulas, but if I do a mistake, I'll have to correct it in every spreadsheet every time.
Is that possible to have a formula which take the cell at:
MODELE!AE58
And automatically generate the same formulas in every tabs:
CHARACTER1!AE58
CHARACTER2!AE58
etc...
Sorry if its blur, I'm doing my best to explain.
simple
Try
function onEdit(e) {
var sh = e.source.getActiveSheet()
var rng = e.source.getActiveRange()
if (rng.getFormula() != '' && sh.getName() == 'MODEL') {
var excl = ['MODEL', 'OTHER'];//excluded sheets
SpreadsheetApp.getActiveSpreadsheet().getSheets().forEach(sh => {
if (!~excl.indexOf(sh.getSheetName())) {
sh.getRange(rng.getA1Notation()).setFormula(rng.getFormula())
}
})
}
}
when you change a formula in MODEL, this will also change in other tabs excepts excluded ones
multiple
If you edit the formulas by dragging them into the MODEL sheet, use this one which allows you to edit all the formulas at once
function onEdit(e) {
var sh = e.source.getActiveSheet()
if (sh.getName() != 'MODEL') return;
for (var i = e.range.rowStart; i <= e.range.rowEnd; i++) {
for (var j = e.range.columnStart; j <= e.range.columnEnd; j++) {
if (sh.getRange(i, j).getFormula() != '') {
var excl = ['MODEL', 'OTHER'];//excluded sheets
SpreadsheetApp.getActiveSpreadsheet().getSheets().forEach(child => {
if (!~excl.indexOf(child.getSheetName())) {
child.getRange(sh.getRange(i, j).getA1Notation()).setFormula(sh.getRange(i, j).getFormula())
}
})
}
}
}
}
global
Il you need to reset all formulas, enable google sheets api and try
function onOpen() {
SpreadsheetApp.getUi().createMenu('⇩ M E N U ⇩')
.addItem('👉 Apply all formulas from MODEL to all tabs', 'spreadFormulas')
.addToUi();
}
function spreadFormulas() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('MODEL')
if (sh.getName() != 'MODEL') return;
var data = [];
var formulas = sh.getRange(1, 1, sh.getLastRow(), sh.getLastColumn()).getFormulas()
for (var i = 0; i < formulas.length; i++) {
for (var j = 0; j < formulas[0].length; j++) {
if (formulas[i][j] != '') {
var excl = ['MODEL', 'OTHER'];//excluded sheets
SpreadsheetApp.getActiveSpreadsheet().getSheets().forEach(child => {
if (!~excl.indexOf(child.getSheetName())) {
data.push({
range: `${child.getName()}!${columnToLetter(+j + 1) + (+i + 1)}`,
values: [[`${formulas[i][j]}`]],
})
}
})
}
}
}
if (data.length) {
var resource = {
valueInputOption: 'USER_ENTERED',
data: data,
};
try { Sheets.Spreadsheets.Values.batchUpdate(resource, ss.getId()); } catch (e) { console.log(JSON.stringify(e)) }
}
}
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;
}
if your sheet is called MODELE try on some other sheet just:
=MODELE!AE58
for array it would be:
={MODELE!AE58:AE100}
also take a look into "Named Ranges" - maybe you will find it more handy

Grouping multiple moving averages in dc.js

I have a dc.js example going where I'd like to calculate moving averages over two different windows of the dataset and group them. Ultimately, I'd like to also group the ratio between the two moving averages with them so I can access them using a key.
I've got a handle on how to do single moving avg using reductio, but I'm not sure how to do two of them concurrently, make a ratio, and show all three (MA1,MA2,Ratio) in the graph.
Here is how I'm doing each independent MA:
var date_array = [];
var mapped_date_array = [];
var activities_infinity = activityDistanceByDayGroup.top(Infinity);
var i = 0;
for (i=0; i < activities_infinity.length; i++) {
date_array.push(activities_infinity[i].key);
}
date_array.sort(function (date1, date2) {
if (date1 > date2) return 1;
if (date1 < date2) return -1;
})
mapped_date_array = date_array.map(function(e) { return e.toDateString();
});
// For Chronic Load
var cLoadMovingAvg = activityByDay.groupAll();
cReducer = reductio().groupAll(function(record) {
var idx = mapped_date_array.indexOf(record.dtg.toDateString());
if (record.dtg < date_array[9]) {
return [date_array[idx]];
} else {
var i = 0;
var return_array = [];
for (i = 9; i >= 0; i--) {
return_array.push(date_array[idx - i]);
}
return return_array;
}
}).count(true).sum(dc.pluck('Distance')).avg(true)(cLoadMovingAvg);
// For Acute Load
var aLoadMovingAvg = activityByDay.groupAll();
aReducer = reductio().groupAll(function(record) {
var idx = mapped_date_array.indexOf(record.dtg.toDateString());
if (record.dtg < date_array[3]) {
return [date_array[idx]];
} else {
var i = 0;
var return_array = [];
for (i = 3; i >= 0; i--) {
return_array.push(date_array[idx - i]);
}
return return_array;
}
}).count(true).sum(dc.pluck('Distance')).avg(true)(aLoadMovingAvg);
jsFiddle is here: http://jsfiddle.net/gasteps/hLh5frc8/2/
Thanks so much for any help on this!

How to create all possible variations from single string presented in special format?

Let's say, I have following template.
Hello, {I'm|he is} a {notable|famous} person.
Result should be
Hello, I'm a notable person.
Hello, I'm a famous person.
Hello, he is a notable person.
Hello, he is a famous person.
The only possible solution I have in mind - full search, but it is not effective.
May be there is a good algorithm for such kind of job but I do not know what task about. All permutations in array is very close to this but I have no idea how to use it here.
Here is working solution (it's part of object, so here is only relevant part).
generateText() parses string and converts 'Hello, {1|2}, here {3,4}' into ['Hello', ['1', '2'], 'here', ['3', '4']]]
extractText() takes this multidimensional array and creates all possible strings
STATE_TEXT: 'TEXT',
STATE_INSIDE_BRACKETS: 'INSIDE_BRACKETS',
generateText: function(text) {
var result = [];
var state = this.STATE_TEXT;
var length = text.length;
var simpleText = '';
var options = [];
var singleOption = '';
var i = 0;
while (i < length) {
var symbol = text[i];
switch(symbol) {
case '{':
if (state === this.STATE_TEXT) {
simpleText = simpleText.trim();
if (simpleText.length) {
result.push(simpleText);
simpleText = '';
}
state = this.STATE_INSIDE_BRACKETS;
}
break;
case '}':
if (state === this.STATE_INSIDE_BRACKETS) {
singleOption = singleOption.trim();
if (singleOption.length) {
options.push(singleOption);
singleOption = '';
}
if (options.length) {
result.push(options);
options = [];
}
state = this.STATE_TEXT;
}
break;
case '|':
if (state === this.STATE_INSIDE_BRACKETS) {
singleOption = singleOption.trim();
if (singleOption.length) {
options.push(singleOption);
singleOption = '';
}
}
break;
default:
if (state === this.STATE_TEXT) {
simpleText += symbol;
} else if (state === this.STATE_INSIDE_BRACKETS) {
singleOption += symbol;
}
break;
}
i++;
}
return result;
},
extractStrings(generated) {
var lengths = {};
var currents = {};
var permutations = 0;
var length = generated.length;
for (var i = 0; i < length; i++) {
if ($.isArray(generated[i])) {
lengths[i] = generated[i].length;
currents[i] = lengths[i];
permutations += lengths[i];
}
}
var strings = [];
for (var i = 0; i < permutations; i++) {
var string = [];
for (var k = 0; k < length; k++) {
if (typeof lengths[k] === 'undefined') {
string.push(generated[k]);
continue;
}
currents[k] -= 1;
if (currents[k] < 0) {
currents[k] = lengths[k] - 1;
}
string.push(generated[k][currents[k]]);
}
strings.push(string.join(' '));
}
return strings;
},
The only possible solution I have in mind - full search, but it is not effective.
If you must provide full results, you must run full search. There is simply no way around it. You don't need all permutations, though: the number of results is equal to the product of the number of alternatives in each template.
Although there are multiple ways to implement this, recursion is among the most popular approaches. Here is some pseudo-code to get you started:
string[][] templates = {{"I'm", "he is"}, {"notable", "famous", "boring"}}
int[] pos = new int[templates.Length]
string[] fills = new string[templates.Length]
recurse(templates, fills, 0)
...
void recurse(string[][] templates, string[] fills, int pos) {
if (pos == fills.Length) {
formatResult(fills);
} else {
foreach option in templates[pos] {
fills[pos] = option
recurse(templates, fills, pos+1);
}
}
}
It seems like the best solution here is going to be n*m where n=the first array and m= the second array . There are nm required lines of output, which means that as long as you are only doing nm you aren't doing any extra work
The generic running time for this is where there is more than 2 arrays with options, it would be
n1*n2...*nm where each of those is equal to the size of the respective list
A nested loop where you just print out the value for the current index of the outer loop along with the current value for the index of the inner loop should do this properly

InDesign: ExtendScript to list fonts and extended font information

I need to list detailed information about the fonts used in a set of inDesign documents. The information I need is essentially accessible through the menu item Type › Find Fonts… (as explained here) but going through each font in every document and writing down the information is not feasible.
I can find much of the information in the Font objects underdocument.fonts and my question is how to access or generate the extended properties found in the panel below:
Character count for the given font
Pages where the font occurs
Edit: The document.fonts array also doesn't seem to include missing fonts.
Well, here's a brute-force strategy for character counting. It iterates through every character textStyleRange in the document and checks its applied font. Edit: Updated to use textStyleRanges. Much faster than going through every character.
var document = app.open(new File(Folder.desktop.fsName + "/test/test.indd"));
try {
var fontMultiset = countCharsInFonts(document);
// For each font, display its character count.
var fonts = document.fonts.everyItem().getElements();
for (var i = 0; i < fonts.length; i++) {
var fontName = fonts[i].fullName;
$.writeln(fontName + ": " + fontMultiset[fontName]);
}
}
finally {
document.close();
}
function countCharsInFonts(document) {
// Create the font multiset.
var fontMultiset = {
add: function add(fontName, number) {
if (this.hasOwnProperty(fontName)) {
this[fontName] += number;
}
else {
this[fontName] = number;
}
},
};
// For every textStyleRange in the document, add its applied font to the multiset.
var stories = document.stories.everyItem().getElements();
for (var i = 0; i < stories.length; i++) {
var story = stories[i];
var textStyleRanges = story.textStyleRanges.everyItem().getElements();
for (var j = 0; j < textStyleRanges.length; j++) {
fontMultiset.add(textStyleRanges[j].appliedFont.fullName, textStyleRanges[j].length);
}
}
// For any fonts that aren't applied in the document, set the character count to 0.
var fonts = document.fonts.everyItem().getElements();
for (var i = 0; i < fonts.length; i++) {
var fontName = fonts[i].fullName;
if (!fontMultiset.hasOwnProperty(fontName)) {
fontMultiset[fontName] = 0;
}
}
return fontMultiset;
}

Resources