The script inserted copies the values in range Sheet1!A5:F5 then transpose pastes into the first row and second column of Sheet2.
The script is able to copy and transpose paste under the last row of the second column on continuous use until it is reset.
.
Current Result Before Paste - Copies Range Sheet1 A5:F5
Current Result After Paste - Pastes to Range 2nd Column in Sheet2
.
.
.
My aim would be:
for the script to work with the checkbox in cell B2.
to create a target range for the values to be pasted to:
Sheet2! B7:B - hopefully still allowing the transpose paste to function through getLastRow()+1.
Expected Result Before Paste - Checkbox = True, Copying Range Sheet1 A5:F5
Expected Result After Paste - Paste to Range Sheet2 B7:B
Have tried inserting:
if (sheet.getSheetName() != "*DuplioMltSlds" || range.getA1Notation() != "D13" || !range.isChecked()) return;
as well as changing the function to onEdit.
Have also tried various var row = ss.getRange("Sheet2!B7:B") options, however, have not been able find a suitable result.
Sample Sheet
Script source and script:
function copyTransposeAndDelete () {
var ss = SpreadsheetApp.getActiveSpreadsheet ();
var source = ss.getRange("Sheet1!A5:F5");
var destSheet = ss.getSheetByName("Sheet2");
var destRange = destSheet.getRange(destSheet.getLastRow()+1, 2, source.getWidth(), source.getHeight());
destRange.setValues(transpose(source.getValues()));
}
function transpose(array) {
return array[0].map(function(row, i) {
return array.map(function(col) {
return col[i];
});
});
}
I believe your goal is as follows.
You want to copy and paste the cells "A5:F5" of "Sheet1" to the cell "B2" of "Sheet2". In this case, you want to copy the values by transposing.
When the values are copied again, you want to paste the transposed values to (the last row + 1) of column "B" of "Sheet2".
You want to run the script when the checkbox of cell "B2" of "Sheet1" is checked.
In this case, how about the following modification?
Modified script:
function onEdit(e) {
// Ref: https://stackoverflow.com/a/44563639
Object.prototype.get1stNonEmptyRowFromBottom = function (columnNumber, offsetRow = 1) {
const search = this.getRange(offsetRow, columnNumber, this.getMaxRows()).createTextFinder(".").useRegularExpression(true).findPrevious();
return search ? search.getRow() : offsetRow;
};
var { source, range } = e;
var sheet = range.getSheet();
if (sheet.getSheetName() != "Sheet1" || range.getA1Notation() != "B2" || !range.isChecked()) return;
var srcRange = sheet.getRange("A5:F5");
var dstSheet = source.getSheetByName("Sheet2")
var row = dstSheet.get1stNonEmptyRowFromBottom(2) + 1;
var dstRange = dstSheet.getRange("B" + (row < 7 ? 7 : row));
srcRange.copyTo(dstRange, SpreadsheetApp.CopyPasteType.PASTE_VALUES, true);
}
In your situation, I thought that copyTo(destination, copyPasteType, transposed) might be suitable. Because, in this method, the values can be copied by transposing.
When the OnEdit trigger is used, you can use the event object. By this, the process cost will become low a little. Ref
Note:
When you use this script, please check the checkbox of "B2" of "Sheet1". By this, the script is run. When you directly run the script with the script editor, an error like TypeError: Cannot destructure property 'source' of 'e' as it is undefined. occurs. Please be careful about this.
References:
copyTo(destination, copyPasteType, transposed)
Event Objects
Added:
If you want to put the values to the column "B" of "Sheet2" even when the column "B" of "Sheet2" has already had the values after row 7, how about the following sample script?
Sample script:
function onEdit(e) {
// Ref: https://stackoverflow.com/a/44563639/7108653
Object.prototype.get1stEmptyRowFromTop = function (columnNumber, offsetRow = 7) {
const range = this.getRange(offsetRow, columnNumber, 2);
const values = range.getDisplayValues();
if (values[0][0] && values[1][0]) {
return range.getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow() + 1;
} else if (values[0][0] && !values[1][0]) {
return offsetRow + 1;
}
return offsetRow;
};
var { source, range } = e;
var sheet = range.getSheet();
if (sheet.getSheetName() != "Sheet1" || range.getA1Notation() != "B2" || !range.isChecked()) return;
var srcRange = sheet.getRange("A5:F5");
var dstSheet = source.getSheetByName("Sheet2")
var row = dstSheet.get1stEmptyRowFromTop(2);
var dstRange = dstSheet.getRange("B" + row);
srcRange.copyTo(dstRange, SpreadsheetApp.CopyPasteType.PASTE_VALUES, true);
}
function myfunk() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet1")
const vs = sh.getRange("A4:F4").getValues().flat().map(e => [e]);
const dsh = ss.getSheetByName("Sheet2");
dsh.getRange(7,2,vs.length,vs[0].length).setValues(vs)
}
Related
The goal is to create a script that automatically adds strikethrough to text across a row when the last cell in a row's value is changed to "FOUND!"
This is what I have so far:
function strikethroughRow(row) {
const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Records");
const firstCell = "B" + row;
const secondCell = "A" + row;
firstCell.setFontLine("line-through");
secondCell.setFontLine("line-through");
}
function onEdit(e) {
const column = e.getColumn();
const value = e.getValue();
if(column === 3.0 && value === "FOUND!"){
const row = e.getRow();
strikethroughRow(row);
}
}
From your question and your showing script, I believe your goal is as follows.
When a value of "FOUND!" is put into column "C", you want to set the strikethrough to columns "A" and "B".
Modification points:
In your script, e.getColumn(), e.getValue() and e.getRow() are not correct. In this case, it is required to use e.range.
firstCell and secondCell are string value. By this, an error occurs.
I thought that in this case, the script might be able to be simpler. So, how about the following modification?
Modified script:
function onEdit(e) {
const sheetName = "Sheet1"; // Please set your sheet name.
const { range } = e;
const sheet = range.getSheet();
const value = range.getValue();
if (sheet.getSheetName() != sheetName || range.columnStart != 3 || value != "FOUND!") return;
sheet.getRange(range.rowStart, 1, 1, sheet.getLastColumn() - 1).setFontLine("line-through");
}
Note:
If the last column is not column "C", how about the following modified script?
function onEdit(e) {
const sheetName = "Sheet1"; // Please set your sheet name.
const { range } = e;
const sheet = range.getSheet();
const value = range.getValue();
const lastColumn = sheet.getLastColumn();
if (sheet.getSheetName() != sheetName || range.columnStart != lastColumn || value != "FOUND!") return;
sheet.getRange(range.rowStart, 1, 1, sheet.getLastColumn() - 1).setFontLine("line-through");
}
Reference:
Simple Triggers
Let's say certain cells will be protected if it matches certain condition
function A() {
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getActiveSheet();
var protection;
for(var i = 3;i<ss.getLastRow();i=i+1){
if(ss.getRange(i,4).getValue()==7){ // check if the condition is matched
for(var j=5; j<=20; j=j+2){
if(ss.getRange(i,j).isBlank()){
protection = ss.getRange(i,j).protect(); // protect that certain cell
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit())
protection.setDomainEdit(false);
}
}
}
}
Now I want to delete the protection I made on that cell if the condition changed, let's say the upcoming code will be:
if(ss.getRange(i,4).getValue()<=6){
...
How can I only delete the protection applied to that cell instead of all the protection in the entire sheet?
Thank you
I tried
getProtections().remove()
but it does not target certain cells right?
I believe your goal is as follows.
You want to unprotect the cells when the value of column "D" is less than 6.
About Now I want to delete the protection I made on that cell if the condition changed, when I saw your showing script for protecting the cells, it seems that when the if-statement of if(ss.getRange(i,4).getValue()==7){} is true, the empty cells of the row are protected. From this situation, in your goal, when the if statement of if(ss.getRange(i,4).getValue()<=6){} is true, you want to remove all protected cells of the row.
If my understanding is correct, how about the following sample script? In this case, the protected ranges can be retrieved with getProtections(SpreadsheetApp.ProtectionType.RANGE).
Sample script 1:
In this sample, when this script is run, the protected cells are unprotected by searching the values of column "D".
function myFunction() {
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getActiveSheet();
var protects = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE).reduce((o, p) => {
var row = p.getRange().getRow();
o[row] = o[row] ? [...o[row], p] : [p];
return o;
}, {});
var values = ss.getRange(3, 4, ss.getLastRow() - 2, 1).getValues();
values.forEach(([d], i) => {
if (d <= 6) {
var obj = protects[i + 3];
if (obj) {
obj.forEach(p => p.remove());
}
}
});
}
Sample script 2:
In this sample, when the cells of column "D" is edited, the script is run by the installable OnEdit trigger. So, please install the OnEdit trigger to the function installedOnEdit. When you use this script, please edit the cells of column "D". By this, the script is run. When this script is directly run by the script editor, an error occurs. Please be careful about this.
function installedOnEdit(e) {
var sheetName = "Sheet1"; // Please set your sheet name.
var { range } = e;
var sheet = range.getSheet();
if (sheet.getSheetName() != sheetName || range.columnStart != 4 || range.rowStart < 3 || range.getValue() > 6) return;
var protects = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE).reduce((o, p) => {
var row = p.getRange().getRow();
o[row] = o[row] ? [...o[row], p] : [p];
return o;
}, {});
var obj = protects[range.rowStart];
if (obj) {
obj.forEach(p => p.remove());
}
}
Note:
When I saw your script A(), I thought that when the script is modified, the process cost might be able to be reduced a little. So, how about the following modification?
function A() {
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getActiveSheet();
var values = ss.getRange(3, 1, ss.getLastRow() - 2, 20).getValues();
values.forEach((r, i) => {
if (r[3] == 7) {
for (var j = 4; j < 20; j = j + 2) {
if (r[j].toString() == "") {
protection = ss.getRange(i + 3, j + 1).protect();
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit())
protection.setDomainEdit(false);
}
}
}
});
}
References:
getProtections(type)
reduce()
forEach()
Installable Triggers
I have two working functions. One of them is myFunction with a trigger. Is protects the row of the cell when any information is entered in this cell in Column4.
function myFunction(e) {
const sheetNames = ['Sheet1', 'Sheet2', 'Sheet3']; // Please set the sheet names you want to run the script.
const range = e.range;
const sheet = range.getSheet();
const value = range.getValue();
const row = range.getRow();
if (!sheetNames.includes(sheet.getSheetName()) || range.getColumn() != 4 || row == 2 || value == "") return;
const p = sheet.getRange(`B${row}:D${row}`).protect();
const owner = Session.getActiveUser().getEmail();
p.getEditors().forEach(f => {
const email = f.getEmail();
if (email != owner) p.removeEditor(email);
});
}
Another function is an onEdit function. It adds date in Column1 when I enter information in Column4. The date appears in the same row with the cell in Column4.
function onEdit() {
var colToCheck = 4;
// Offset from the input [row, column]
var dateOffset = [0, -3];
// Sheets to proceed on
var sheetNames = ['Sheet1', 'Sheet2', 'Sheet3'];
var sheet = SpreadsheetApp.getActive().getActiveSheet();
var name = sheet.getName();
if (sheetNames.indexOf(name) > -1) {
var cell = sheet.getActiveCell();
var col = cell.getColumn();
if (col == colToCheck) {
var dateTimeCell = cell.offset(dateOffset[0], dateOffset[1]);
dateTimeCell.setValue(new Date());
}
}
}
How these two functions can be combined in one sheet?
I just added two separate functions to one sheet: the one with trigger and the one onEdit as two different codes. And they work as I need. So, we do not need to combine them somehow. They just work one after another. First, the one onEdit function works, and as it adds info to the necessary cell, the function with trigger starts working.
I am trying to create a sort function using Google Apps Script. I have about 20 columns in a Google sheet and want users to have the ability to sort the sheet by the click of a button rather than using the filter view because they keep on messing it up.
Rather than having 20 buttons for each column, I want one button with script which links to a dropdown list of Named Ranges being the same as the column headers.
Not sure if this is possible but this is a sample of my sheet:
I am struggling to get this script to work:
function sortByRangeName(rangeName){
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('Sheet 1');
var namedRange = ss.getRangeByName();
var startCol = namedRange.getColumn();
var lastCol = namedRange.getLastColumn();
var range = sheet.getRange('E1');
var value = range.getValue();
var rangeName = ss.getRangeByName(value);
var columnForSorting = (startCol <= dataRange.getLastColumn()) ? startCol : null;
if (namedRange && (startCol == lastCol) && columnForSorting) {
dataRange.sort({column: columnForSorting, ascending: false});
}
else {
throw new Error(Utilities.formatString("Range name: %s, startCol: %s, lastCol: %s, columnForSorting: %s", header, startCol, lastCol, columnForSorting));
}
}
This is a link to my spreadsheet:
Sample Spreadsheet
Try an Installable onEdit() with this function:
function sortByColumn(e) {
const sh=e.range.getSheet();
if(sh.getName()=='Sheet 1' && e.range.columnStart==5 && e.range.rowStart==1 && e.value) {
const hA=sh.getRange(2,1,1,sh.getLastColumn()).getValues()[0];
const col={};
hA.forEach(function(h,i){col[h]=i+1;});
const rg=sh.getRange(3,1,sh.getLastRow()-2,sh.getLastColumn());
rg.sort({column:col[e.value],ascending:true});
}
}
I believe your goal as follows.
You want to sort the range of "A3:E" with each column using the named ranges when the dropdown list at the cell "E1" on "Sheet 1" is selected.
Modification points:
In your script,
At var namedRange = ss.getRangeByName(), the argument is not used.
dataRange is not declared.
When above issues are resolved, the script works. But in this case, unfortunately, your goal cannot be achieve.
In order to achieve above goal, in this answer, the OnEdit event trigger is used.
In your dropdown list, it seems that there is the names including the space. Please be careful this. For this, I used trim().
Modified script:
Please copy and paste the following script to the script editor on the Google Spreadsheet. And, please select the dropdown list. By this, the values are sorted using the named ranges with the column selected by the dropdown list.
function onEdit(e) {
var range = e.range;
var sheet = range.getSheet();
if (sheet.getSheetName() != "Sheet 1" || range.getA1Notation() != "E1") return;
sheet.getDataRange().offset(2, 0).sort({
column: e.source.getRangeByName(e.value.trim()).getColumn(),
ascending: false
});
}
Note:
When the issues are removed from your script, it becomes as follows. I thought that knowing the modification points in your script might be help to study the script. So I also added this.
function sortByRangeName(rangeName){
rangeName = "Branch"; // This is a sample value
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('Sheet 1');
var namedRange = ss.getRangeByName(rangeName.trim()); // Modified
var startCol = namedRange.getColumn();
var lastCol = namedRange.getLastColumn();
var range = sheet.getRange('E1');
var value = range.getValue();
var rangeName = ss.getRangeByName(value);
var dataRange = sheet.getDataRange().offset(2, 0); // Added
var columnForSorting = (startCol <= dataRange.getLastColumn()) ? startCol : null;
if (namedRange && (startCol == lastCol) && columnForSorting) {
dataRange.sort({column: columnForSorting, ascending: false});
} else {
throw new Error(Utilities.formatString("Range name: %s, startCol: %s, lastCol: %s, columnForSorting: %s", header, startCol, lastCol, columnForSorting));
}
}
References:
Simple Triggers
Event Objects
getRangeByName(name)
trim()
In this spreadsheet: https://docs.google.com/spreadsheets/d/1givNbMvgzD8lbk6NAcwjkpp4-A_D8MetltHjEpinOAI/edit#gid=0
I'd like to Combine only unique Links and Category into Combined sheet.
Right now, with my script, it can only combine all existing data:
function combine() {
var ss = SpreadsheetApp.getActive();
var allsheets = ss.getSheets();
var sourceID = '1givNbMvgzD8lbk6NAcwjkpp4-A_D8MetltHjEpinOAI';
var targetID = '1givNbMvgzD8lbk6NAcwjkpp4-A_D8MetltHjEpinOAI';
var sheetExclude = ["Combined"];
var sheetExcludeIndex = new Array(sheetExclude.length);
for (var s in allsheets) {
var sheet = allsheets[s];
for (var e in sheetExclude) {
if (String(sheet.getName() == sheetExclude[e])) {
sheetExcludeIndex[e] = sheet.getIndex;
}
}
}
allsheets.splice(sheetExcludeIndex, sheetExclude.length);
for (var s in allsheets) {
var sheet = allsheets[s];
updateSourceToTarget(sourceID, sheet.getName(), targetID, 'Combined');
}
}
function updateSourceToTarget(sourceID, sourceName, targetID, targetname) {
Logger.log(sourceID + ' ' + sourceName + ' ' +targetname);
var source = SpreadsheetApp.openById(sourceID).getSheetByName(sourceName);
var destination = SpreadsheetApp.openById(targetID).getSheetByName(targetname);
var sourcelastRow = source.getLastRow();
var sourcelastCol = source.getLastColumn();
var destinationlastRow = destination.getLastRow();
var destinationlastCol = destination.getLastColumn();
var sourcedata = source.getRange(2, 9, sourcelastRow, 10).getValues();
destination.getRange(destinationlastRow + 1, 2, sourcelastRow, sourcelastCol).setValues(sourcedata);
}
However, I'd like to only combine unique links from Sheet2 and Sheet3:
In red is unique data
Sheet2:
Sheet3:
How can I efficiently add only unique values to Combined from Sheet2& Sheet3?
You want to put the values, which removed the duplicated links, to the target sheet (in this case, it's Combined sheet.).
The duplicated links are checked from the target sheet and source sheets. In this case, the target sheet and source sheets are Combined, Sheet2 and Sheet3, respectively.
In your sample Spreadsheet, you want to put the following rows to the target sheet.
https://thehill.com/policy/national-security/department-of-homeland-security/460158-new-hampshire-border-patrol BorderSecurity
https://abcnews.go.com/International/climate-change-frontier-worlds-northernmost-town/story?id=65381362 ClimateChange
You want to achieve this by modifying your Google Apps Script.
If my understanding is correct, how about this modification? Please think of this as just one of several answers.
Modified script:
Please modify your script as follows. In this modification, your function of updateSourceToTarget() is not used.
From:
for (var s in allsheets) {
var sheet = allsheets[s];
updateSourceToTarget(sourceID, sheet.getName(), targetID, 'Combined');
}
To:
// Retrieve values from the target sheet.
var targetSheet = ss.getSheetByName(sheetExclude[0]);
var targetValues = targetSheet.getRange("B2:C" + targetSheet.getLastRow()).getValues();
// Retrieve values from all source sheets. <--- Modified
var sourceValues = allsheets.reduce(function(ar, sheet) {
var v = sheet.getRange(2, 9, sheet.getLastRow() - 1, 10).getValues().filter(function(r) {return r[0] && r[1]});
if (v.length > 0) {
v = v.filter(function(e) {return !ar.some(function(f) {return e[0] === f[0]})});
Array.prototype.push.apply(ar, v);
}
return ar;
}, []);
// Remove the duplication values between the target sheet and all source sheets.
var dstValues = sourceValues.filter(function(e) {return !targetValues.some(function(f) {return e[0] === f[0]})});
// Add the result values to the target sheet.
if (dstValues.length > 0) {
var destination = SpreadsheetApp.openById(targetID).getSheetByName(sheetExclude[0]);
destination.getRange(destination.getLastRow() + 1, 2, dstValues.length, dstValues[0].length).setValues(dstValues);
}
The flow of this modified script is as follows.
Retrieve values from the target sheet.
Retrieve values from all source sheets.
Remove the duplication values between the target sheet and all source sheets.
Add the result values to the target sheet.
Note:
When your shared Spreadsheet is used as the target (Combined) and source sheets (Sheet2 and Sheet3), the following rows are added to the target sheet.
https://thehill.com/policy/national-security/department-of-homeland-security/460158-new-hampshire-border-patrol BorderSecurity
https://abcnews.go.com/International/climate-change-frontier-worlds-northernmost-town/story?id=65381362 ClimateChange
References:
reduce()
filter()
some()
If I misunderstood your question and this was not the direction you want, I apologize.
Added:
In this additional script, a hash table is used for this situation, as mentioned by TheMaster's comment. For example, a sample can be also seen at this thread. In your situation, at first, all values are retrieved from all sheets including Combined sheet, and the hash table is created. By this, the duplicated values are removed. Then, the converted values to an array are put to the Spreadsheet.
Sample script:
Please modify your script as follows.
From:
allsheets.splice(sheetExcludeIndex, sheetExclude.length);
for (var s in allsheets) {
var sheet = allsheets[s];
updateSourceToTarget(sourceID, sheet.getName(), targetID, 'Combined');
}
To:
// allsheets.splice(sheetExcludeIndex, sheetExclude.length); // In this script, this line is not used.
// Retrieve values from the target sheet.
var targetSheet = ss.getSheetByName(sheetExclude[0]);
var targetValues = targetSheet.getRange("B2:C" + targetSheet.getLastRow()).getValues();
// Retrieve values from all source sheets.
// Remove the duplication values between the target sheet and all source sheets.
var sourceValues = allsheets.reduce(function(obj, sheet) {
var v = sheet.getRange(2, 9, sheet.getLastRow() - 1, 10).getValues().filter(function(r) {return r[0] && r[1]});
if (v.length > 0) v.forEach(function(e) {if (!(e[0] in obj)) obj[e[0]] = e[1]});
return obj;
}, {});
var dstValues = Object.keys(sourceValues).map(function(e) {return [e, sourceValues[e]]});
// Add the result values to the target sheet.
if (dstValues.length > 0) {
var destination = SpreadsheetApp.openById(targetID).getSheetByName(sheetExclude[0]);
destination.getRange(2, 2, destination.getLastRow(), 2).clearContent();
destination.getRange(2, 2, dstValues.length, dstValues[0].length).setValues(dstValues);
}
The proposed 2 sample scripts can be used for your situation. So please select one of them.