I want to copy the names to another tab as well as their costs, and then do the total cost of all similar items.
My logic for the script is to compare each value to the previous copied value and if it is different then the output should be "Total" but if it is the same then the "Name" should be copied. (I basically want to copy the data row by row, value by value, so that I can put certain conditions based on which the copying should happen or not.
The script I have written so far is only for copying the names to the target sheet but even that isn't working out for me. My knowledge is clearly limited, if anyone could help out, that would be great!
This is the doc that I am using, if anyone wants to refer to.
Attached images for those who can't access it:
Source Data:
How I want the target sheet to look like
function myFunction() {
Copy();
}
function Copy() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ts = ss.getSheetByName("Total");
for (var n=1;n<10;n++) {
var namesSourceRange = ss.getSheetByName("Breakdown").getRange(1,2,n,1);
var namesTargetRange = ss.getSheetByName("Total").getRange(1,2,n,1);
var sourceNames = namesSourceRange.getValues();
var targetNames = namesTargetRange.getValues();
if (n=1){
namesTargetRange.setValues(sourceNames);
}
else{
if (sourceNames[n] === targetNames[n-1]){
namesTargetRange.setValues(sourceNames);
}
else {
namesTargetRange.setValue("Total");
}
}
}
}
You could do the following:
Sort the source data according to the Names column, using sort.
Iterate through the source data, for example, using forEach. Push each row values to another array destData which will be used to copy the data to the destination.
If the row's name is not the same as the next one (that means it's the last row corresponding to this Name), or if there's no next one (that is to say, it's the last row in the source data), calculate the Total for this name, using filter and reduce, and then push that total to destData.
Use setValues(values) to copy destData to the destination sheet.
Code sample:
function Copy() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ts = ss.getSheetByName("Totals");
var sourceSheet = ss.getSheetByName("Breakdown");
var sourceData = sourceSheet.getRange(2,2,sourceSheet.getLastRow()-1,2).getValues();
sourceData.sort((a,b) => {
if (a[0] < b[0]) return 1;
else return -1;
});
const destData = [["Names", "Cost"]];
sourceData.forEach((row, i) => {
destData.push(row);
if (sourceData.length === i + 1 || row[0] !== sourceData[i+1][0]) {
const total = sourceData.filter(rowData => rowData[0] === row[0]).map(row => row[1]).reduce((acc,current) => acc+current);
destData.push(["Total", total], ["", ""])
}
});
ts.getRange(1,2,destData.length, destData[0].length).setValues(destData);
}
Related
I have a google sheet with the following data
google sheet "formatfruit"
Each user has a fruit and a vegetable associated, I want to know the percentage of similarity between each user in the google sheet "formatfruit"
Today I can compare the first user kevin with all the others and return his percentage of similarity in another google sheet called "matchofruit".
I associated the value "1" when a user has a fruit or a vegetable in common with kevin and the value "0" if the user has no fruit or vegetable in common.
The result that appears in the google sheet matchofruit is here
google sheet matchofruit
The code I used is below
function myFunction() {
var formafruit = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("fruit");
var matchofruit = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("matchofruit");
var n = formafruit.getLastRow();
var user1 = formafruit.getRange(2,1).getValues();// name user 1 : kévin
var user2 = formafruit.getRange(3,1).getValues();// name user 2 : mikael
for (var i = 2;i<4;i++) { // i<4 because we have 3 column in formafruit
for (var z = 2;z<n+1;z++) {
matchofruit.getRange(z,1).setValue(user1); // Return the name of the users in the first column
if(formafruit.getRange(2,i).getValue() === formafruit.getRange(z,i).getValue()){ // Compare the fruits and vegetables associated to kévin with the fruits and vegetables associated to each user
matchofruit.getRange(z,i).setValue(1); // Returns 1 if kevin shares at least one fruit or vegetable in common with a user
}
else {
matchofruit.getRange(z,i).setValue(0);
}
}
}
// Calculate the % of common values
for (var p = 0;p<n-1;p++){}
for (var s = 0;s<n-1;s++) {
var scoreforall = matchofruit.getRange(2,2,p,11).getValues()[s]// get the array of all the matches
let sum = 0;
for (let e = 0; e < scoreforall.length; e++) {
sum += scoreforall[e]; // add each array together
}
var sumTotal= Math.round(sum*(100/2)); // convert in percentage each sum
matchofruit.getRange(s+2,4).setValue(sumTotal); // send match values in column 4
}
// Return the result in a sentence
for (var a = 2;a<n+1;a++) {
var usern = formafruit.getRange(a,1).getValues(); //get all the users' emails in the formafruit
var valeurmatch = matchofruit.getRange (a,4).getValues(); // get value % of matches
matchofruit.getRange(a,5).setValue(user1+" "+"have"+" "+valeurmatch+"%"+" "+"of values in common with"+" "+usern);//Return the % of common value between Kevin and the other users
}
}
I would like to be able to do the same for mikael, gauthier, vanessa and mireille knowing that I only put 5 users to simplify the problem but that in truth there can be more than 100 users and that each user has more than 11 associated values(here we have only 2 different type of values, fruits and vegetables). It's been several weeks that I'm looking for a solution to my problem and I haven't found anything to solve it. Do you have an idea?
Thanks!
I believe your goal is as follows.
You want to achieve the following situations. (The following images are from OP's question.)
From
To
In your situation, for example, when 5 users are used, you want to create 25 rows.
When I saw your script, the methods of setValues and getValues are used in the loop. I think that this becomes the high process cost. Ref So, I would like to propose the following modification.
Modified script:
function myFunction2() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const [src, dst] = ["fruit", "matchofruit"].map(s => ss.getSheetByName(s));
const [, ...values] = src.getDataRange().getValues();
const res = values.flatMap(([a1, ...v1]) =>
values.map(([a2, ...v2]) => {
const temp = v1.map((e, i) => e == v2[i] ? 1 : 0);
const sum = (temp.filter(f => f == 1).length / temp.length) * 100;
const matchResult = `${a1} have ${sum}% of values in common with ${a2}`;
return [a1, ...temp, sum, matchResult];
})
);
dst.getRange(2, 1, res.length, res[0].length).setValues(res);
}
In this modification, the values are retrieved from "fruit" sheet. And, an array for putting to "matchofruit" sheet is created. And then, the created array is put into "matchofruit" sheet.
Note:
In this sample script, the header row of "matchofruit" has already been put. If you want to put the header row to "matchofruit" sheet, please add it to my proposed script.
References:
map()
filter()
I have sheet A and B, data from sheet A which matching my criteria I am duplicating on sheet B by using formulas. Since some data not matching my criteria I am getting an empty row.
**PROBLEM STATEMENT **
I have a script which I am planning to use to delete empty rows on the sheet. But I have been notice that its deleting all formulas same time and all formatting.
I need to delete the rows only between the non empty ones. For example row 1,2 have data, row 5 have data, so, only row 3-4 need to be removed, this process should be automatic.
CODE
function deleteEmptyRows(){
var sh = SpreadsheetApp.getActiveSheet();
var data = sh.getDataRange().getValues();
var targetData = new Array();
for(n=0;n<data.length;++n){
if(data[n].join().replace(/,/g,'')!=''){ targetData.push(data[n])};
Logger.log(data[n].join().replace(/,/g,''))
}
sh.getDataRange().clear();
sh.getRange(1,1,targetData.length,targetData[0].length).setValues(targetData);
}
I see no sample of your data and have no idea how your sheet looks like, so here is just another guess:
function deleteEmptyRows() {
const sh = SpreadsheetApp.getActiveSheet();
const data = sh.getDataRange().getValues();
const empty_rows = [];
for (let i in data) if (data[i].join('') == '') empty_rows.push(+i+1);
empty_rows.reverse().forEach(x => sh.deleteRow(x));
}
Update
The variant of the function that finds for the last row contains visible data and removes all empty (with no visible data) rows above:
function delete_all_empty_rows_above_the_last_filled_row() {
const sh = SpreadsheetApp.getActiveSheet();
const data_all = sh.getDataRange().getValues();
for (var last_row = data_all.length-1; last_row>0; last_row--) {
if (data_all[last_row].join('') != '') break;
}
const data = sh.getRange(1,1,last_row+1,3).getValues();
const empty_rows = [];
for (let i in data) if (data[i].join('') == '') empty_rows.push(+i+1);
empty_rows.reverse().forEach(x => sh.deleteRow(x));
}
Update 2
To make the function to file every time as there were changes on the sheet "IF Copy" within the column "A" you need to add something like this:
function onEdit(e) {
if (e.range.getSheet().getName() != "IF Copy") return;
if (e.range.columnStart == 1)
delete_all_empty_rows_above_the_last_filled_row();
}
But onEdit() trigger works only when you edit sheet manually. If a sheet changes via some formula or another function the trigger doesn't work.
I'm trying to automate hyperlink creations on my GSheet.
Here's my script:
function ticketURLGenerator() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Data");
var range = sheet.getRange("C2:C");
var ticketID = range.getValue();
Logger.log(ticketID);
for(i = 0; i < ticketID.length; i++){
if(ticketID.length === 0){
ticketID.setValue('')
} else if(ticketID.length > 4){
ticketID.setValue('=HYPERLINK' + '("https://mylink.com/'+ticketID+'";'+ticketID+')');
}
}
}
It does nothing but when I change ticketID.setValue by sheet.getRange("C2:C").setValue it put the whole range in the url. We can see with Logger.log(ticketID) that the whole range is selected.
So according to this result, i'm missing how to get the value of each cell individualy in the range and then check if they are long enought to create an individual url. Do I need to use something like range[i] somewhere? I'm lost.
I believe your goal as follows.
You want to retrieve the values from the cells "C2:C".
When the length of value is more than 4, you want to create a formula of HYPERLINK.
When the length of value is less than 4, you don't want to put the formula.
You want to put the formulas to the cells "C2:C".
Modification points:
When range of var range = sheet.getRange("C2:C") is used, the value of var ticketID = range.getValue() is the value of cell "C2". When you want to retrieve values from the cells "C2:C", please use getValues instead of getValue.
In this case, the retrieved value is 2 dimensional array.
When range.getValue() is the string value, ticketID of var ticketID = range.getValue() is also the string. So I think that when ticketID.setValue('##') is run, an error occurs.
In your script, setValue is used in a loop. In this case, the process cost will become high.
And, when sheet.getRange("C2:C" + sheet.getLastRow()) is used instead of sheet.getRange("C2:C"), the process cost will become low a little.
When above points are reflected to your script, it becomes as follows.
Modified script:
function ticketURLGenerator() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Data");
var range = sheet.getRange("C2:C" + sheet.getLastRow());
var ticketIDs = range.getValues();
var values = ticketIDs.map(([c]) => [c.toString().length > 4 ? `=HYPERLINK("https://mylink.com/${c}";"${c}")` : c]);
range.setValues(values);
}
In this modification, the values are retrieved from the cells of "C2:C" + sheet.getLastRow(), and an array including the formulas and values is created, and then, the array is put to the cells.
And I used the template literal for creating the formula.
Note:
In this case, please use this script with enabling V8 runtime.
References:
getLastRow()
getValues()
map()
Template literals
You just need to apply the HYPERLINK operation to the tickets that their length is more than 4. To achieve that, you can use map() to iterate over all the elements in your list.
Solution:
function ticketURLGenerator() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Data");
const ticketR = sheet.getRange("C2:C"+sheet.getLastRow());
const ticketIDs = ticketR.getDisplayValues().flat();
const hLinks = ticketIDs.map(ti=>{
if(ti.length>4) {
return [`=HYPERLINK("https://mylink.com/${ti}"; ${ti})`]}
else {return [ti]}
})
ticketR.setValues(hLinks);
}
So I have combinations of names to tasks in a table where several different task are associated with the same name. But I need to put the task into one cell next to the associated name. Using JavaScript. Heres what I got;
function Unique(){
var ss = SpreadsheetApp.openById("ID");
var dataRaw = ss.getSheetByName("Sheet1");
var destination = ss.getSheetByName("Sheet2");
var names2 = dataRaw.getRange(2,10,dataRaw.getLastRow(),1).getValues();
var names1 = names2.flat(1);
var names = names1;
//var names = ["name1","name1","name2", "name3", "name3"];
var uniqueNames = []; //empty array
var count = 0;
var found = false;
for (i = 0; i < names.length; i++){
for(y =0; y < uniqueNames.length; y++){
if(names[i] == uniqueNames[y]){
found = true;
}
}
count++;
if(count == 1 && found == false){
uniqueNames.push(names[i]);
}
count = 0;
found = false;
}
/* can I use this??? maybe it's not needed
var uniqueNames2 = uniqueNames.map(function(obj) {
return Object.keys(obj).sort().map(function(key) {
return obj[key];
});
});
*/
var dest = destination.getRange(1,2,uniqueNames.length,uniqueNames[0].length);
dest.setValue(uniqueNames); //maybe this is not needed
console.log(uniqueNames[0].length);
}
My approach is to;
take in names and output the unique names so there is no doubles
once i have unique names use some type of for() loop or map() function to find tasks and pair with names? maybe im wrong?
and then setValues() to the range that I need.
The problems that I'm running into are that My Unique() function needs a regular array not array of arrays, which i fix using
array.flat(1)
but then to paste the values javaScript needs the array or arrays to be just an array which I COULD fix with
Object.keys(obj).sort().map(function(key)
in the commented out section? to turn an array of arrays back into an array... but then my "width" is not consistent for my array, columns, and I get the error that my range is not the same number of columns as my data. I feel that this is fairly simple and I am grossly over complicating things. Any help would be great thank you. My google sheet below https://docs.google.com/spreadsheets/d/1rbz52kkzhVAGX21MUVoexzPUvWxjk-RCw-5PrRLoBBc/edit?usp=sharing
I believe your goal as follows.
You want to achieve the following conversion.
From
Task Names
Task 1 name one
Task 2 name one
Task 3 name one
Task 4 name one
Task 5 name one
Task 1 name two
Task 2 name two
Task 3 name two
Task 1 name three
Task 2 name three
Task 3 name three
To
task names
Task 1
Task 2
Task 3
Task 4
Task 5 Name one
Task 1
Task 2
Task 3 name two
Task 1
Task 2
Task 3 name three
For this, how about this answer?
Modification points:
In your script, for example, about var names2 = dataRaw.getRange(2,10,dataRaw.getLastRow(),1).getValues();, I thought that you might misunderstand the row and column for getRange. And, in this case, only one row Names of column "B" on "Sheet1" is retrieved. The row of Task is not retrieved in your script. And also, from dest.setValue(uniqueNames);, you might misuderstood setValue and setValues.
When above points are reflected to your script, it becomes as follows.
Modified script:
In this modification, at name2, the values from the cells "B2:B12" are retrieved, and the unique values are retrieved using your script. Then, the values from the cells "A2:B12" are retrieved, and the values for putting to Spreadsheet are created using the created unique values. Then, the created values are put to the Spreadsheet.
Modified script:
function Unique_org2(){
var ss = SpreadsheetApp.openById("ID");
var dataRaw = ss.getSheetByName("Sheet1");
var destination = ss.getSheetByName("Sheet2");
var names2 = dataRaw.getRange(2,2,dataRaw.getLastRow()-1,1).getValues(); // <--- Modified
var names1 = names2.flat(1);
var names = names1;
var uniqueNames = [];
var count = 0;
var found = false;
for (i = 0; i < names.length; i++){
for(y =0; y < uniqueNames.length; y++){
if(names[i] == uniqueNames[y]){
found = true;
}
}
count++;
if(count == 1 && found == false){
uniqueNames.push(names[i]);
}
count = 0;
found = false;
}
// --- I added below script.
var values = dataRaw.getRange(2, 1, dataRaw.getLastRow() - 1, 2).getValues(); // Added
var uniqueNames = uniqueNames.reduce((ar, e) => {
var temp = "";
values.forEach(([a, b]) => {
if (e == b) temp += a + "\n";
});
ar.push([temp.trim(), e]);
return ar;
}, []);
// ---
var dest = destination.getRange(2,1,uniqueNames.length,uniqueNames[0].length); // <--- Modified
dest.setValues(uniqueNames); // <--- Modified
}
Other pattern:
In this pattern, in order to achieve your goal, I would like to propose the other sample script of following flow. This flow might be able to reduce the process cost from above modified script.
Retrieve values from the cells "A2:B12" of "Sheet1".
Create an object from the retrieved values.
Convert the object to an array for putting to Spreadsheet.
Put the values to Spreadsheet to the destination sheet.
Sample script:
function Unique(){
var ss = SpreadsheetApp.openById("ID");
var dataRaw = ss.getSheetByName("Sheet1");
var destination = ss.getSheetByName("Sheet2");
// 1. Retrieve values from the cells "A2:B12" of "Sheet1".
const values = dataRaw.getRange(2, 1, dataRaw.getLastRow() - 1, 2).getValues();
// 2. Create an object from the retrieved values.
const obj = values.reduce((o, [a, b]) => Object.assign(o, {[b]: (o[b] ? o[b] + a : a) + "\n"}), {});
// 3. Convert the object to an array for putting to Spreadsheet.
const res = Object.entries(obj).map(([k, v]) => [v.trim(), k]);
// 4. Put the values to Spreadsheet to the destination sheet.
destination.getRange(2, 1, res.length, res[0].length).setValues(res);
}
References:
getRange(row, column, numRows, numColumns)
setValue(value)
setValues(values)
I have a similar situation to the one described on this question: two worksheets, with input data coming into the Feed sheet using the importxml function and a Data sheet where new rows get copied thanks to a script set to run daily.
However, the current script is creating daily duplicates. As such, I would like to adapt the answer provided on the question above so that the script checks the IDs on column F and only copies the rows with new IDs.
How should I update the section below that creates a hash to one that looks for the IDs on column F instead? Also my rows are consistent, so is it correct to assume I can just remove the relevant code lines towards the end?
The sample Google Sheet is available here.
function appendUniqueRows() {
var ss = SpreadsheetApp.getActive();
var sourceSheet = ss.getSheetByName('Get Data');
var destSheet = ss.getSheetByName('Final Data');
var sourceData = sourceSheet.getDataRange().getValues();
var destData = destSheet.getDataRange().getValues();
// Check whether destination sheet is empty
if (destData.length === 1 && "" === destData[0].join('')) {
// Empty, so ignore the phantom row
destData = [];
}
// Generate hash for comparisons
var destHash = {};
destData.forEach(function(row) {
destHash[row.join('')] = true; // could be anything
});
// Concatentate source rows to dest rows if they satisfy a uniqueness filter
var mergedData = destData.concat(sourceData.filter(function (row) {
var hashedRow = row.join('');
if (!destHash.hasOwnProperty(hashedRow)) {
// This row is unique
destHash[hashedRow] = true; // Add to hash for future comparisons
return true; // filter -> true
}
return false; // not unique, filter -> false
}));
// Check whether two data sets were the same width
var sourceWidth = (sourceData.length > 0) ? sourceData[0].length : 0;
var destWidth = (destData.length > 0) ? destData[0].length : 0;
if (sourceWidth !== destWidth) {
// Pad out all columns for the new row
var mergedWidth = Math.max(sourceWidth,destWidth);
for (var row=0; row<mergedData.length; row++) {
for (var col=mergedData[row].length; col<mergedWidth; col++)
mergedData[row].push('');
}
}
// Write merged data to destination sheet
destSheet.getRange(1, 1, mergedData.length, mergedData[0].length)
.setValues(mergedData);
}
I'm a novice in this world of Google Apps scripts, so do please let me know if I'm missing any crucial information. Thanks in advance for the help.
You want to copy the values from "Feed" sheet to "Data" sheet.
When the values are copied, you want to copy only new values which are not included in "Data" sheet.
You want to choose the new values using the values of column "F".
If my understanding for your question is correct, how about this modification? In this modification, I modified the script in your shared spreadsheet.
Modification points:
In your script, all values of "Feed" sheet are copied to "Data" sheet. So in order to choose only new values, I used the following flow.
Retrieve the values from column "F". This is used for choosing the new values.
Retrieve the new values using the values from column "F".
Put the new values to "Data" sheet.
The script which reflected above flow is as follows.
Modified script:
From:
This is your script in the shared spreadsheet. Please modify this script to below one.
function Copy() {
var sss = SpreadsheetApp.openById('#####'); // this is your Spreadsheet key
var ss = sss.getSheetByName('Feed'); // this is the name of your source Sheet tab
var range = ss.getRange('A3:H52'); //assign the range you want to copy
var data = range.getValues();
var tss = SpreadsheetApp.openById('#####'); //replace with destination ID
var ts = tss.getSheetByName('Data'); //replace with destination Sheet tab name
ts.getRange(ts.getLastRow()+1, 1,50,8).setValues(data);// 49 value refers to number of rows, 8 to columns
}
To:
function Copy() {
var sss = SpreadsheetApp.openById('#####'); // this is your Spreadsheet key
var ss = sss.getSheetByName('Feed'); // this is the name of your source Sheet tab
var range = ss.getRange('A3:H52'); //assign the range you want to copy
var data = range.getValues();
var tss = SpreadsheetApp.openById('#####'); //replace with destination ID
var ts = tss.getSheetByName('Data'); //replace with destination Sheet tab name
// Below script was added.
var values = ts.getRange("F3:F").getValues().filter(String);
var copiedValues = data.filter(function(e) {return !values.some(function(f){return f[0] == e[5]}) && e.filter(String).length > 0});
ts.getRange(ts.getLastRow() + 1, 1, copiedValues.length, copiedValues[0].length).setValues(copiedValues);
}