Google Scripts: Match Range and Update corresponding Value - google-apps-script

Sheet1: DashBoard | Range: A7:B17
(B7: Has a Filter Function that displays results of all Open Tasks from BackEnd Sheet)
Sheet2: BackEnd | Range: J56:M
Match B7:B17 with M56:M
if there is a match then
Paste values from A7:A17 to J56:J
Click here for Sample Sheet
The below script does the above operation but ONLY on the first instance of running it. Trying to repeat the process does not do anything. No errors on the code.
I'm a beginner to any form of coding and the below code was scripted by referring to various solutions on the forum. I might be missing some logic here and not in a position to de-code it in order to achieve the desired end result.
Thanks in advance
function updateToDo2() {
var sh, toDo, status, update, i;
sh = SpreadsheetApp.getActive().getSheetByName('DashBoard')
toDo = sh.getRange('B7:B17').getValues();
status = sh.getRange('A7:A17').getValues();
update = SpreadsheetApp.getActive().getSheetByName('BackEnd').getRange('J56:M');
update.setValues(update.getValues()
.map(function(r, i) {
if (r[3] == toDo[i]) {
r[0] = status[i];
}
return r;
})
)
}
EDIT: Working Code but very slow. Needs optimization.
Please help!
function rowOfStatus(status) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('BackEnd');
var data = sheet.getRange('M56:M60').getValues();
for (var i = 0; i < data.length; i++) {
if (data[i][0] == status) { //[1] because column B
return i + 1;
}
}
}
function updateToDo() {
var sh, sh1, dashboard, results, noteText, status, i, counter;
sh = SpreadsheetApp.getActive().getSheetByName('DashBoard');
sh1 = SpreadsheetApp.getActive().getSheetByName('BackEnd');
dashboard = sh.getRange('A7:B11');
results = dashboard.getDisplayValues();
results.forEach(function(row) {
status = row[0];
noteText = row[1];
counter = Number(rowOfStatus(noteText));
if (counter > 0 && status != '') {
counter = 55 + Number(rowOfStatus(noteText));
sh1.getRange('J' + counter).setValue(status);
}
});
}

I'm not completely understanding what your doing but I thought I'd take a stab and trying to help you. So this might not be what you want. In that case sorry to bother you.
function updateToDo2() {
var sh=SpreadsheetApp.getActive().getSheetByName('DashBoard')
var toDo=sh.getRange('B7:B17').getValues();
var status=sh.getRange('A7:A17').getValues();
var update=SpreadsheetApp.getActive().getSheetByName('BackEnd').getRange('J56:M'+SpreadsheetApp.getActive().getSheetByName('BackEnd').getLastRow());
var vA=update.getValues();
for(var i=0;i<vA.length;i++){
if(vA[i][3]==toDo[i]){
vA[i][0]=status[i];
}
}
update.setValues(vA);
}

I got help in optimizing this code from Google Docs Help Forum and was provided by Jean-Pierre. Since I first posted my query on that Forum and was advised to post my query on StackOverflow by it's community member. Hence, here is the solution just in case it helps someone here. It's short & pretty fast!
Google Forum Link
Code:
function updateToDo() {
var ss, arr, values, colB, backendValues, backendColM;
ss = SpreadsheetApp.getActive();
arr = [];
values = ss.getSheetByName('DashBoard').getRange('A7:B17').getValues()
colB = values.map(function (v) {
return v[1];
})
backendValues = ss.getSheetByName('BackEnd').getRange('J56:M').getValues()
backendColM= backendValues.map(function (bv) {return bv[3]}).forEach(function (el, i) {
arr[i] = (el && colB.indexOf(el) > -1) ? ['Closed', 'Cancelled'].indexOf(values[colB.indexOf(el)][0]) > -1 ? [values[colB.indexOf(el)][0]] : [backendValues[i][0]]
: [backendValues[i][0]];
})
ss.getSheetByName('BackEnd').getRange(56, 10, arr.length, 1).setValues(arr)
ss.getSheetByName('DashBoard').getRange('A7:A17').setValue(null);
}

Related

Google Sheets Sorting resets my function values even though they are cached

I have a pet project in google sheets that keeps track of the tv shows i've watched and syncs them up with the data in imdb (via some free api from www.omdbapi.com) to tell me what show I'm falling behind on, and should continue watching. I have a free account and I can only make 1k requests per day, so caching is an absolute MUST -- which is why there is caching EVERYWHERE. Issue I'm having is that onEdit runs, and sorts everything (expected), but re runs all the functions and I get these "Loading..." all over the place even though ALL of the functions are cached, I suspect google is making some of these fail?! But they don't "Error" (in the "Execution" tab) and they say "Completed" with no logs.
Does anyone know where the bottleneck is? And more importantly, how to fix it?
Here is the read only link to the sheet to try it out.
/**
* #OnlyCurrentDoc
*/
var APIKEY = 'xxxxxxx';
var CACHE_URL = 60 * 30; // 30 min
var CACHE_FUNCTION = 60 * 60 * 24; // 1 day
function onEdit(event) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var range = sheet.getRange("A3:K100");
range.sort([
{column: 9, ascending: false}, // continue watching
{column: 10, ascending: false}, // my ratings
{column: 8, ascending: true}, // days since
]);
ss.toast('Sort complete.');
}
function _cacheResult(key, callback, format, ttl) {
var cache = CacheService.getScriptCache();
var cached = cache.get(key);
if (cached != null) {
console.log(`found in cache: ${key}`, cached);
return format(cached);
}
var data = callback();
if (data == null) {
console.log(`cannot cache: ${key}`);
return data;
}
console.log(`cached!: ${key}`, data);
cache.put(key, data, ttl);
return format(data);
}
function _getJSON(url) {
var key = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, url)
.map(function(chr){return (chr+256).toString(16).slice(-2)})
.join('');
var callback = function() {
var response = UrlFetchApp.fetch(url)
var rawData = response.getContentText();
console.log(response.getResponseCode(), rawData);
if (response.getResponseCode() == 200) {
return rawData;
}
return null;
}
return _cacheResult(key, callback, JSON.parse, CACHE_URL)
}
function getSeasonDetail(imdbId, season) {
return _getJSON(`https://www.omdbapi.com/?i=${imdbId}&apikey=${APIKEY}&season=${season}`);
}
function _getValidEpisode(episodes) {
var currentDate = new Date();
console.log(episodes);
for (var i = episodes.length - 1; i >= 0; i--) {
if (episodes[i]['Released'] == 'N/A') {
console.log(`Skipping episode ${i+1}: unreleased`);
continue;
}
var dateSince = (currentDate - (new Date(episodes[i]['Released'])));
console.log(`Episode ${i+1}: ${dateSince}`);
if (dateSince > 0) {
console.log(`Episode ${i+1}: found!`);
return episodes[i];
}
}
return null;
}
function getIMDBRating(imdbId) {
if (!imdbId) {
return '';
}
var callback = function() {
var data = _getJSON(`https://www.omdbapi.com/?i=${imdbId}&apikey=${APIKEY}`);
return data['imdbRating'];
}
return _cacheResult(`rating:${imdbId}`, callback, parseFloat, CACHE_FUNCTION);
}
function getCurrentSeason(imdbId) {
if (!imdbId) {
console.log(`Ignoring.. bad id`, imdbId);
return '';
}
var callback = function() {
var data = _getJSON(`https://www.omdbapi.com/?i=${imdbId}&apikey=${APIKEY}`);
var season = data['totalSeasons'];
while (season) {
var seasonDetail = getSeasonDetail(imdbId, season);
var episode = _getValidEpisode(seasonDetail['Episodes']);
if (episode === null) {
console.log(`Season ${season} has no episodes ${imdbId}`);
season -= 1;
continue;
}
return season;
}
}
return _cacheResult(`current_season:${imdbId}`, callback, parseInt, CACHE_FUNCTION);
}
function getCurrentSeasonEpisode(imdbId, season) {
if (!imdbId) {
console.log(`Ignoring.. bad id`, imdbId);
return '';
} else if (!season || isNaN(parseInt(season))) {
console.log(`Ignoring ${imdbId}.. bad season id`, season);
return '?';
}
var callback = function() {
var data = getSeasonDetail(imdbId, season);
var episode = _getValidEpisode(data['Episodes']);
if (episode === null) {
console.log(`Ignoring.. bad episode response`, episode);
return '?';
}
return episode['Episode'];
}
return _cacheResult(`current_episode:${imdbId}:${season}`, callback, parseInt, CACHE_FUNCTION);
}
function getCurrentSeasonEpisodeReleased(imdbId, season) {
if (!imdbId) {
console.log(`Ignoring.. bad id`, imdbId);
return '';
} else if (!season || isNaN(parseInt(season))) {
console.log(`Ignoring ${imdbId}.. bad season id`, season);
return '?';
}
var callback = function() {
var data = getSeasonDetail(imdbId, season);
var episode = _getValidEpisode(data['Episodes']);
if (episode === null) {
console.log(`Ignoring.. bad episode reponse`, episode);
return '?';
}
return episode['Released'];
}
return _cacheResult(`current_episode_released:${imdbId}:${season}`, callback, String, CACHE_FUNCTION);
}
Issue and workaround:
I think that in your situation when onEdit is run, the rows are sorted. By this, the cell coordinates used by the formulas are also changed. By this, the formulas are recalculated. I think that this might be the reason for your issue.
If you don't want to recalculate the formulas, how about the following workaround?
Fix the values from the formulas, that the HTTP request is run, to the values.
In this case, even when the rows are sorted, UrlFetchApp.fetch(url) is not run.
When you want to update the values, the fixed values are updated by putting the formulas in the cells.
With this workaround, I thought that the quotas of the API might be able to be reduced.
In order to use this workaround, please add the following 2 functions.
Sample script:
Please copy and paste the following script to the script editor of your Spreadsheet.
// Values of columns E,F,G and K are fixed.
function fixValues() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var lastRow = sheet.getLastRow();
sheet.getRangeList(["E3:G" + lastRow, "K3:K" + lastRow]).getRanges().forEach(r => r.copyTo(r, { contentsOnly: true }));
}
// In order to update values, formulas are put to columns E,F,G and K.
function updateValues() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var lastRow = sheet.getLastRow();
var formulas = [
"=getCurrentSeason(R[0]C[-4])", // To column E
"=getCurrentSeasonEpisode(R[0]C[-5], R[0]C[-1])", // To column F
"=getCurrentSeasonEpisodeReleased(R[0]C[-6], R[0]C[-2])", // To column G
"=getIMDBRating(R[0]C[-10])", // To column K
];
sheet.getRangeList(["E3:E" + lastRow, "F3:F" + lastRow, "G3:G" + lastRow, "K3:K" + lastRow]).getRanges().forEach((r, i) => r.setFormulaR1C1(formulas[i]));
}
fixValues(): This function is used for fixing the values of columns E, F, G, and K.
updateValues(): This function is used for updating the values of columns E, F, G, and K using the formulas.
Testing:
In order to test this script, please do the following flow.
Please run fixValues() to your provided sample Spreadsheet. By this, the cells which have the formulas are fixed as the values.
Please edit the cells. By this, the rows are sorted. In this case, the formulas are not recalculated because of no formulas.
Please run updateValues(). By this, the values are updated by putting the formulas to the columns E, F, G, and K.
I thought that by this flow, your issue might be removed.
Note:
This sample script is for your provided sample Spreadsheet. So, when you change the structure of the Spreadsheet, this script might not be able to be used. Please be careful about this.
References:
getRangeList(a1Notations)
copyTo(destination, options)
setFormulaR1C1(formula)

Improve loading efficiency of App Script code in Google Sheets

ISSUE
I have a spreadsheet whereby I generate the end column based on the other columns present. I do this using the app script code below. There are now 1147 rows in this spreadsheet and I often notice a long period of loading to retrieve all of the rows.
Are there any suggestions on how I can improve the efficiency and responsiveness?
EXAMPLE
ARRAY FORMULA ON END COLUMN
=ARRAYFORMULA(
IF(A2:A="Spec", "# SPEC "&B2:B,
IF(A2:A="Scenario", "## "&B2:B,
IF(A2:A="Step", "* "&TAGS(C2:C,D2:D),
IF(A2:A="Tag", "Tags: "&REGEXREPLACE(B2:B,"\s",""),
IF(A2A="", ""))))))
APP SCRIPT CODE
Utilities.sleep(3000)
/** #OnlyCurrentDoc */
function TAGS(input,textreplacement) {
if (input.length > 0) {
var lst = input.split(",")
var rep = textreplacement.match(/<[^>]*>/g)
for (i in rep){
textreplacement = textreplacement.replace(rep[i],'"'+lst[i]+'"')
}
return textreplacement
}
else{
return textreplacement
}
}
EDIT
From the image below I would like to replace everything with triangle brackets < > in column D, with the values in column C, separated by comma.
I use the Array Formula in column E to do an initial conversion and then use the TAGS function to add in the values.
Ideally I would use the Array Formula in one cell at the top of column E to do all the replacements.
Custom functions in Google Apps Script tend to take long time to process and I wouldn't recommend to use it in several cells. I would like to understand better what you trying to do with this data in order to answer properly, but anyway, I would try one of these two solutions:
1 - Inline formula:
Using only native functions has a better performance. Not sure how you could achieve this, since you are iterating inside that TAGS function.
2- Calculate values interely with Script and replace values in column E:
You could create a function that may run from onEdit event or get activated by a custom menu. Generally it would be like this:
function calculateColumnE() {
var sheet = SpreadsheetApp.openById('some-id').getSheetByName('some-name');
var row_count = sheet.getLastRow();
var input_data = sheet.getRange(1, 1, row_count, 4).getValues();
var data = [];
for (var i = 0; i < row_count; i++) {
var row_data; // this variable will receive value for column E in this row
/*
...
manage input_data here
...
*/
data.push([row_data]); // data array MUST be a 2 dimensional array
}
sheet.getRange(1, 5, data.length, 1).setValues(data);
}
EDIT
Here is the full code for solution 2:
function TAGS(input,textreplacement) { //keeping your original function
if (input.length > 0) {
var lst = input.split(",")
var rep = textreplacement.match(/<[^>]*>/g)
for (i in rep){
textreplacement = textreplacement.replace(rep[i],'"'+lst[i]+'"')
}
return textreplacement
}
else{
return textreplacement
}
}
function calculateColumnE() {
var sheet = SpreadsheetApp.openById('some-id').getSheetByName('some-name');
var row_count = sheet.getLastRow();
var input_data = sheet.getRange(1, 1, row_count, 4).getValues();
var data = [];
for (var i = 0; i < row_count; i++) {
var row_data; // this variable will receive value for column E in this row
if (input_data[i][0] == "Spec") {
row_data = "# SPEC " + input_data[i][1];
} else if (input_data[i][0] == "Scenario") {
row_data = "## " + input_data[i][1];
} else if (input_data[i][0] == "Step") {
row_data = "* " + TAGS(input_data[i][2], input_data[i][3]);
} else if (input_data[i][0] == "Tag") {
row_data = "Tags: " + input_data[i][1].replace(/\s/, ''); // not sure what this is doing
} else if (input_data[i][0] == "") {
row_data = "";
}
data.push([row_data]); // data array MUST be a 2 dimensional array
}
sheet.getRange(1, 5, data.length, 1).setValues(data);
}
I also created a working example, which you can check here: https://docs.google.com/spreadsheets/d/1q2SYD7nYubSuvkMOKQAFuGsrGzrMElzZNIFb8PjM7Yk/edit#gid=0 (send me request if you need it).
It works like a charm using onEdit event to trigger calculateColumnE() with few lines, I'm curious to know about the result in your 1000+ rows sheet. If it get slow, you may need to run this function manually.
Not sure if this will be faster:
function TAGS(input,tr) {
if (input.length > 0) {
var lst = input.split(",");
var i=0;
tr=tr.replace(/<[^>]*>/g,function(){return '"' + lst[i++] + '"';});
}
return tr;
}

google sheets script run when rows are added and copy results to different sheet

I've been running a script which splits comma separated data into new rows through a function on a different sheet from the source data. The amount of data has grown and the calculation is taking forever.
taking forever progress bar
The example spreadsheet is here (with small amount of data):
https://docs.google.com/spreadsheets/d/1e0YhJqHn62jju7KOOy5U2JZ5FLCv6_oeY-mc_jRF5-4/edit?usp=sharing
The script is the following:
function extract(range, colToSplit, delimiter) {
var resArr = [], row;
range.forEach(function (r) {
r[colToSplit-1].replace(/(?:\r\n|\r|\n)(\d|\w)/g," , ").split(delimiter)
.forEach(function (s) {
row = [];
r.forEach(function (c, k) {
row.push( (k === colToSplit-1) ? s.trim() : c);
})
resArr.push(row);
})
})
return resArr;}
The question is, how to run this script for new data only and copy the result into a different "static without-formulas" worksheet in a way where the script is not run with every change for ALL the data.
My highest regards to the script gods.
Instead of using a cell function, you can use a script and trigger it through the editor or by using a button in your sheet. By doing so, you will benefit of their 6 minute time limit (see the quotas).
The function can be modified as such, and then assign it to a button to be run as an onClick event (more about this here):
function extract() {
var resArr = [], row;
var colToSplit = 6;
var delimiter = " , ";
range = SpreadsheetApp.getActive().getSheets()[0].getDataRange().getValues();
range.forEach(function (r) {
r[colToSplit-1].replace(/(?:\r\n|\r|\n)(\d|\w)/g," , ").split(delimiter)
.forEach(function (s) {
row = [];
r.forEach(function (c, k) {
row.push( (k === colToSplit-1) ? s.trim() : c);
})
resArr.push(row);
})
})
SpreadsheetApp.getActive().getSheets()[1].getRange(1, 1, resArr.length, resArr[0].length).setValues(resArr);
}
Please let me know if you have any other questions.
You can create the html for your function in a sidebar very simply as follows:
function showButtonDialog() {
var ss=SpreadsheetApp.getActive();
var html='<input type="text" id="rng" /> Range<br /><input type="text" id="col" /> Column<br /><input type="text" id="dlm" /> Delimiter<br /><input type="button" value="run extract" onClick="extract();" />';
html+='<script>function extract() {google.script.run.extract(document.getElementById("rng").value,document.getElementById("col").value,document.getElementById("dlm").value);}</script>';
var userInterface=HtmlService.createHtmlOutput(html);
SpreadsheetApp.getUi().showSidebar(userInterface);
}
This will test that your getting the right parameters
function extract(a,b,c) {
Logger.log('rng: %s col: %s del: %s',a,b,c);
}
You'll probably have to tweak your function a bit. But from the looks of your code you'll have no trouble with that.
I have seen that you unaccepted my previous answer; I assume that it does not serve your purpose anymore. Here I provide yet another solution doing what you originally asked - that is processing only the new rows. This is accomplished by use of Script Properties.
See below all the code:
var COL_TO_SPLIT = 6;
var DELIMITER = " , ";
var NUM_COLUMNS = 16;
function extract(range, colToSplit, delimiter) {
var resArr = [];
range.forEach(function (r) {
r[colToSplit-1].replace(/(?:\r\n|\r|\n)(\d|\w)/g," , ").split(delimiter)
.forEach(function (s) {
var row = [];
r.forEach(function (c, k) {
row.push( (k === colToSplit-1) ? s.trim() : c);
});
resArr.push(row);
});
});
return resArr;
}
function getCurrentRow() {
var currentRow = PropertiesService.getScriptProperties().getProperty('currentRow');
if (currentRow === null) {
currentRow = 2;
}
return parseInt(currentRow);
}
function updateCurrentRow(newRow) {
PropertiesService.getScriptProperties().setProperty('currentRow', newRow);
}
// Returns the new "currentRow"
function migrateRowsFrom(row) {
var ss = SpreadsheetApp.getActive();
var sourceSheet = ss.getSheetByName('Sheet1');
var destinationSheet = ss.getSheetByName('Sheet2');
var rowsToProcess = sourceSheet.getLastRow() - row + 1;
if (rowsToProcess <= 0) return sourceSheet.getLastRow() + 1;
var rows = sourceSheet.getRange(row, 1, rowsToProcess, NUM_COLUMNS).getDisplayValues();
var extracted = extract(rows, COL_TO_SPLIT, DELIMITER);
destinationSheet.getRange(destinationSheet.getLastRow() + 1, 1, extracted.length, extracted[0].length).setValues(extracted);
return sourceSheet.getLastRow() + 1;
}
function update() {
var lock = LockService.getDocumentLock();
lock.waitLock(10000); // Wait (up to) 10s to acquire the lock
var currentRow = getCurrentRow(); // currentRow is the row from which the script should run. i.e. if the last execution ended at row 2 (included), the value should be left to be 3. 1-indexed.
var newCurrentRow = migrateRowsFrom(currentRow);
updateCurrentRow(newCurrentRow);
lock.releaseLock();
}
The function that you have to call is update(). It will fetch the row where the last execution left off, process as many rows as possible, and then update that value so the next execution knows where to start from.
Note that this will move the data from Sheet1, process it, and then paste it into Sheet2. Of course, you may change those values as you need. Also, be aware that you should not modify Sheet1 values; since as they have already been processed they will not be processed again.

Remove duplicates values in array Google Apps Script

I would like to know how to remove duplicates values from 2 arrays, combined into one main array.
This values must NOT be removed from the sheets or document, just in the array, thats why I didnt use clear() or clearContents() built in functions;
Ive also tried to modify the removeDuplicates() function from the GAS tutorials, but it throws me rows inside columns from A to Z, instead filtered rows...a total mess.
Notes:
Parameters from getClients() are from others functions, and works ok.
newClients list clients from the sheet 'Users' and newUsers list users from another sheet called 'Data'.
Boths sheets belongs to the same spreadsheet.
newClients and newUsers: both arrays only contains strings (usernames), and in both there are duplicated values.
So the goal is identified and remove those values, the original and the duplicate.
Should be easier I think, but Im new in JS, so everything Ive been tried, didnt worked.
Thanks
The Code
function getAllClientsFromData(body,project){
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Active the Sheets
var sheet= ss.getSheets()[1,3];
// Access data in Sheet "Data"
var rangeC = ss.getRangeByName("clients"); //A:2:C273
var rangeU = ss.getRangeByName("names"); //A5:A
var clients = rangeC.getValues();
var users = rangeU.getValues();
var lastRow = sheet.getLastRow()-2;
body += "<h2>Total of " + lastRow + " clients in " + project + "</h2>"
body += "<table style=" + STYLE.TABLE + ">";
body += getClients(ss,clients,users,project);
body += "</table>";
return body;
}
function getClients(ss,clients,users,project){
var body = "";
var newClients = [];
for( var g = 0; g < clients.length; g++ ){
var currentProject = clients[g][2];
if( clients[g]!= "" ){
newClients.push(clients[g][0]);
}
} // end for
var newUsers = [];
for( var u = 0; u < users.length; u++ ){
if( users[u] != "" ){
newUsers.push(users[u][0]);
}
} // end for
var allData = newUsers.concat(newClients);
var uniqueData = allData.sort();
body += "<tr><td style=" + STYLE.TD + ">" + uniqueData.join("</td><tr><td style=" + STYLE.TD + ">") + "</td></tr></tr>";
return body;
}
UPDATES!
The answers works great filtering, but Im getting the same result as on my previous tries: displaying the filtered results. I need to remove them from the array. example:
var array = ['aa','bb','aa','ff','pp', 'pp'];
filtering code...
var array = ['bb','ff'];
I try to add splice() js method but the params I pass, does not working ok.
The array you are working on is not a 2D array anymore since you extracted the fields before sorting... so you can use a very simple duplicate removal function as shown below with an example and some added Logger.log to see how it works.
function test(){
var array = ['aa','bb','cc','aa','dd','cc']
Logger.log(removeDups(array));
}
function removeDups(array) {
var outArray = [];
array.sort():
outArray.push(array[0]);
for(var n in array){
Logger.log(outArray[outArray.length-1]+' = '+array[n]+' ?');
if(outArray[outArray.length-1]!=array[n]){
outArray.push(array[n]);
}
}
return outArray;
}
in your code this would replace the line
var uniqueData = allData.sort();
that would become :
var uniqueData = removeDups(allData);
EDIT :
If letter case is an issue, you can modify this code to ignore it. You should change the condition and the sort function so that they both ignore the case in your names but preferably keep the original letter case.
This could be achieved with the code below :
function test(){
var array = ['aa','bb','Cc','AA','dd','CC'];// an example with Upper and Lower case
Logger.log(removeDups(array));
}
function removeDups(array) {
var outArray = [];
array.sort(lowerCase);
function lowerCase(a,b){
return a.toLowerCase()>b.toLowerCase() ? 1 : -1;// sort function that does not "see" letter case
}
outArray.push(array[0]);
for(var n in array){
Logger.log(outArray[outArray.length-1]+' = '+array[n]+' ?');
if(outArray[outArray.length-1].toLowerCase()!=array[n].toLowerCase()){
outArray.push(array[n]);
}
}
return outArray;
}
Logger result :
EDIT 2 :
Here is another version that keeps only unique values (I didn't understand correctly your request in the first version as it kept one element from the duplicates...)
I simply added an else if condition to remove the elements that were part of a group of duplicates.(I kept the case insensitive version but you can remove it easily)
function test(){
var array = ['aa','dd','hh','aa','bb','Cc','cc','cc','bb','nn','bb','AA','dd','CC'];// an example with Upper and Lower case
Logger.log('original array = '+array);
Logger.log('unique result = '+removeDups2(array));
}
function removeDups2(array) {
var uniqueArray = []
array.sort(lowerCase);
function lowerCase(a,b){
return a.toLowerCase()>b.toLowerCase() ? 1 : -1;// sort function that does not "see" letter case
}
var temp = array[0];
for(var n=1 ;n<array.length ; n++){
Logger.log(temp+' = '+array[n]+' ?');
if(temp.toLowerCase()!=array[n].toLowerCase()){
uniqueArray.push(array[n]);
temp = array[n];
}else if(uniqueArray[uniqueArray.length-1]==temp){
uniqueArray.pop();// remove it from result if one of the duplicate values
}
}
return uniqueArray;
}
Logger result new code :
In your code you are not doing anything to filter the duplicate values.
This line will just sort the data and won't give you unique data.
var uniqueData = allData.sort();
You can do something like this on your merged array, after you 'installing' 2DArray lib: https://sites.google.com/site/scriptsexamples/custom-methods/2d-arrays-library
var uniqueData = unique(allData);
Another option is to create a loop and check for duplicate values, but you should remember to transform all the values of the string to lowercase before you do these matches.
I created this function and it worked.
function removeDups(data) {
var newData = [];
data.forEach(function(value) {
if (newData.indexOf(value) == -1) {
newData.push(value);
}
});
return newData;
}
Yet another solution:
function removeDups(array) {
array.sort()
var lastValue = !array[0]
var outArray = array.filter(function(value) {
if (value == lastValue)
return false
lastValue = value
return true
})
return outArray
}
This also works correctly for empty arrays whereas some earlier solutions yield [null] in this special case.

Adding a row above 1st counted instance - Google Spreadsheet

I'm attempting to write a script which will enable me to add a single row above the 1st counted instance of a unique value.
Eg.
A
German
German
German
Italian
Italian
French
French
After running the script it should resemble this:
A
DE
German
German
German
IT
Italian
Italian
FR
French
French
What I have written has only gotten as far as identifying how many values are present:
function insertRowAbove()
{
var report = SpreadsheetApp.getActive().getSheetByName('REPORT');
var lang = report.getRange('A10:A').getValues();
var positions = report.getRange('A10:A').getA1Notation();
var DE = [];
for (var i = 0; i < lang.length; i++)
{
if (lang[i] == 'German')
{
DE++
report.getRange('A1').setValue(DE);//now I know there are 3 German entries
}
}
}
My question:
Is it possible for the script to know the A1notation of the 1st occurrence of a value and add a row above it? I thank you for any sagely advice.
Your specific question was:
My question: Is it possible for the script to know the A1notation of
the 1st occurrence of a value and add a row above it?
By using a combination of a Boolean to indicate whether you've found any of the items you are looking for, and a couple of Range methods, you can get the A1notation of the first occurrence.
var column = 1; // this example is only using column A
var foundGerman = false;
var firstGerman = '';
...
if (lang[i] == 'German') {
if (!foundGerman) {
foundGerman = true;
firstGerman = range.getCell(i, column).getA1Notation();
}
...
}
After that, you will have the A1Notation of the first cell containing "German".
However, the function insertRowsBefore() is a Sheet method, and expects a row number as a parameter, not A1Notation. So figuring out what the address of the first German cell was turns out to be unnecessary.
Script
Here's my entry in the contest! For speed, it's wise to use as few apps script service calls as possible. In this script, all data manipulation is done using arrays, with the final result written once.
In anticipation that you'll have more than three languages that you care about, and for maintainability, the language lookup is handled by an Object, iso639. (Assuming you're using ISO 639-1 Language Codes.) As a result, the actual work takes just 10 lines of code!
function insertViaArray() {
var sheet = SpreadsheetApp.getActive().getSheetByName('REPORT');
var values = sheet.getDataRange().getValues();
var newValues = [];
var iso639 =
{
"German" : "DE",
"Italian" : "IT",
"French" : "FR"
}
var curLang = '';
for (var i in values) {
if (values[i][0] !== curLang) {
curLang = values[i][0];
newValues.push([iso639[curLang]]);
}
newValues.push([values[i][0]]);
}
sheet.getRange(1, 1, newValues.length, 1).setValues(newValues)
};
Edit Script V2
By using Array.splice() to insert the language tags, we can further reduce the code to 8 working lines, and eliminate the need for a parallel newValues array.
function insertViaArrayV2() {
var sheet = SpreadsheetApp.getActive().getSheetByName('REPORT');
var values = sheet.getDataRange().getValues();
var iso639 =
{
"German" : "DE",
"Italian" : "IT",
"French" : "FR"
}
var curLang = '';
for (var i = 0; i < values.length; i++) {
if (values[i][0] !== curLang) {
curLang = values[i][0];
values.splice(i, 0, [iso639[curLang]]);
}
}
sheet.getRange(1, 1, values.length, 1).setValues(values)
};
The following works (see EXAMPLE):
function insertRowAbove() {
var report = SpreadsheetApp.getActive().getSheetByName('Insert Row Above');
var values = report.getRange('B2:B').getValues();
var prevVal = "null";
var index = 1;
while (index < values.length) {
if (values[index].toString() != prevVal.toString()) {
var header = 'unknown';
if (values[index] == 'German')
header = 'DE';
else if (values[index] == 'Italian')
header = 'IT';
else if (values[index] == 'French')
header = 'FR';
report.insertRowBefore(index+1);
report.getRange(index+1, 2).setValue(header);
values = report.getRange('B2:B').getValues();
index++;
}
prevVal = values[index];
index++;
}
}
This is kind of a funny exercise and, as usual, there are probably many ways to get it working.
I'm not pretending my approach is better, it's just different and therefor it it probably worth showing it here ;-)
function testFunction() {
var sh = SpreadsheetApp.getActiveSheet();
var data = sh.getRange('A10:A').getValues();
var rowNum = 11;
var previous = data[0][0];
Logger.log(data)
sh.insertRowBefore(10);
sh.getRange(2,1).setValue(firstLetters(data[0][0]));
for(n=1;n<data.length;++n){
if(data[n][0].replace(/ /g,'')==previous.replace(/ /g,'')){
previous = data[n][0] ;
++rowNum ;
continue ;
}else if(firstLetters(data[n][0])){
sh.insertRowBefore(rowNum+1);
sh.getRange(rowNum+1,1).setValue(firstLetters(data[n][0]));
++rowNum
previous = data[n][0]
++rowNum
Logger.log(rowNum+' '+firstLetters(data[n][0]))
}else{
break
}
}
}
function firstLetters(name){
if (name==''){return false}
var str = name.toString().replace(/ /g,'').toUpperCase().substring(0,2);
if (str=='GE') {str='DE'};// handle german exception
if (str=='PO'){ str='PT'} ;//handle portuguese exception
return str;
}