Google Sheets - Name sheet based on value selected from range - google-apps-script

I have a range of week numbers and their corresponding dates in a column J. Week 1: 1/2 - 1/8, and so on. This is populated by:
=ArrayFormula(
LAMBDA(FIRSTOFYEAR,
LAMBDA(DATES,
LAMBDA(WEEKS,
LAMBDA(DATA,
BYROW(ARRAY_CONSTRAIN(DATA,MAX(WEEKS),4),LAMBDA(ROW,JOIN(" ",ROW)))
)(TO_TEXT(QUERY(QUERY({WEEKS,"Week "&WEEKS&":",DATES,DATES},"SELECT Col2,MIN(Col3),'~',MAX(Col4),Col1 GROUP BY Col1,Col2",0),"OFFSET 1 FORMAT Col2'm/d',Col4'm/d'",0)))
)(WEEKNUM(DATES))
)(BYROW(SEQUENCE(365,1,0),LAMBDA(NUM,FIRSTOFYEAR+NUM)))
)(DATE(2023,1,1))
)
I also have a script to get the number of the current sheet:
function getSheetNum() {
const sss = SpreadsheetApp.getActiveSpreadsheet();
const sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (const [index,sheet] of sheets.entries()) {
if(sheet.getName() === sss.getActiveSheet().getName()) return index;
}
}
What I am trying to do, and not doing correctly yet, is to have a script that names the sheet based on the current week. So, the first sheet is sheet 1 and thus will be named Week 1: 1/2 - 1/8. The second sheet is sheet 2 and so it'll be named Week 2: 1/9 - 1/15, and so on. My current, non-working script is below:
function nameSheet() {
var sheet = SpreadsheetApp.getActiveSheet();
var week = getSheetNum();
var oldName = sheet.getName();
var newName = week.getValue();
if (newName.toString().length>0 && newName !== oldName) {
sheet.setName(newName);
}
}
What am I doing wrong??

It will make unnecessary complexity into a function if you have to shift a day from original day and week counts in common sense... which is very much not recommended. And at the same time it may cause unexpected issues when working together with other functions.
but anyway, the following script will create a list of sheet names in the format you want, and set the sheet names with those generated names in ascending order.
class getDate{} is a object class to help modify the Date() object.
createSheetNames(year) is a function which accept an input as to indicate which year of sheet names are you working with, this function will look for the first Monday of the given year to begin with, and return an array of results with a sheet name for each week.
setSheets() is a function which will iterate all sheets you have in your working spreadsheet, and rename each sheet according to the sheet names returned by the last function.
class getDate {
constructor (input) {
if(!!input) this.date = new Date(input);
else this.date = new Date();
}
get year() { return this.date.getFullYear(); }
get month() { return this.date.getMonth() + 1; }
get week() { return this.date.getDay(); }
get day() { return this.date.getDate(); }
calDay(num) {
if(isNaN(num)) throw new Error('requires integer.');
return new getDate(this.date.setDate(this.day + Math.floor(num)));
}
}
function createSheetNames(year) {
const results = [];
const date = new getDate(`${year}-01-01`);
const isInt = (num) => num === Math.floor(num);
while (date.week !== 1) date.calDay(+1);
for (i=0;i<365+7;i++) {
if (new getDate(date.date).calDay(-6).year > year) break; // change -6 of this line to 0 will remove the last week of a year if that week ends inside the next year.
const calWeek = (i + 1)/7;
if (isInt(calWeek)) {
const sheetName = `Week ${calWeek}: ${date.calDay(-6).month}/${date.day} ~ ${date.calDay(+6).month}/${date.day}`;
results.push(sheetName);
}
date.calDay(+1)
}
return results;
}
function setSheets() {
const sheetNames = createSheetNames(2023); // enter the year of sheet name you need here.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheets = ss.getSheets();
for (const [i,sheet] of sheets.entries()) {
if (i > sheetNames.length - 1) break;
sheet.setName(sheetNames[i]);
}
}
After putting these codes into your spreadsheet, run the setSheets() function for one time, it will replace all your sheet names.
2023-01-11 update:
Fixed a couple of typos that may lead to unexpected results, and removed some variables that are not necessary in this using case which may? enchance execution speed very slightly.

You can try this code, If you will start renaming it from Sheet 1 forward although you should set the Activesheet() on the First sheet.:
function renameSheets() {
const ss = SpreadsheetApp.getActive();
var sheets = ss.getSheets();
const weekNames = ss.getActiveSheet().getRange("J1:J").getDisplayValues().flat();
for (i=0; i<sheets.length; i++) {
if(sheets[i].getName() != weekNames[i]){
sheets[i].setName(weekNames[i]);
}
}
}
Result:
Alternatively, you can use this code if you want to have a main/fixed sheet which you will not rename:
function renameSheets2() {
const ss = SpreadsheetApp.getActive();
var sheets = ss.getSheets();
const weekNames = ss.getRange("MAIN!J1:J").getDisplayValues().flat();
for (i=1; i<sheets.length; i++) {
if(sheets[i].getName() != weekNames[i-1]){
sheets[i].setName(weekNames[i-1]);
}
}
}
Result:
For example, I have named my First sheet as Main.

Since you already have the desired sheet names in a spreadsheet range, try renaming sheets to those names, like this:
function nameSheetsByWeek() {
const ss = SpreadsheetApp.getActive();
const weekNames = ss.getRange('Sheet1!A2:A').getDisplayValues().flat();
ss.getSheets().forEach((sheet, index) => {
const newSheetName = weekNames[index - 1];
if (!index || !newSheetName) {
return;
}
sheet.setName(newSheetName);
});
}

Related

Google sheet function into Google script function

I'm using Google Sheets function for analyze some data, but even if i have not huge database, the sheet is lagging with my function. The function is:
=ARRAY_CONSTRAIN(ARRAYFORMULA(SUMIF(IF(A2:A10000="Received",ROW(A2:A10000),""), "<="&ROW(A2:A10000), B2:B10000)+G1-SUMIF(IF(A2:A10000="Given",ROW(A2:A10000),""), "<="&ROW(A2:A10000), B2:B10000)),COUNTA(B2:B10000),1)
Is it possible to use this function via Google script so as not to overload the sheet?
Example sheet: https://docs.google.com/spreadsheets/d/1UeIXFVsP5hevC20D04juTstBbfViYhWUIp6VRst_Nu4
Try this script. It worked correctly in my copy.
The purpose is to take previous value and add or subtract new value depending on the condition in column A
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var previous = s.getRange('G1').getValue(); //ger vaule from last month
var valuecount = s.getLastRow() ; // defines number of rows
for (let i = 1; i < valuecount; i++) { //starts loop
var direction = s.getRange(i+1 , 1).getValue();
if (direction == 'Received') {
previous = previous + s.getRange(i+1 , 2).getValue() ;
}
else
{previous = previous - s.getRange(i+1 , 2).getValue() }
s.getRange(2,4,valuecount).getCell(i,1).setValue(previous);
}
}

AutoFill Data with Blank Rows - Google Sheets / Google Apps Script

I have the below spreadsheet that I would like to AutoFill the persons name. the issue is that there are blank rows between the names. Each name is in line with a sku2 and needs to be inline with all locations. there can be up to 10 blank rows (due to how many locations).
if I could loop this maybe
function LoopTillLr() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('A2').activate();
spreadsheet.getActiveRange().autoFillToNeighbor(SpreadsheetApp.AutoFillSeries.DEFAULT_SERIES);
spreadsheet.getCurrentCell().getNextDataCell(SpreadsheetApp.Direction.DOWN).activate();
};
Appreciate any help
If you only want to replicate the NAME values against variable LOCATION values then use this script:
function myFunction() {
var ss = SpreadsheetApp.getActiveSheet();
var lastRow = ss.getDataRange().getLastRow();
for (var i = 1; i < lastRow+1; i++) {
if (ss.getRange(i,1).getValue() == "") {
var value = ss.getRange(i-1,1).getValue();
ss.getRange(i,1).setValue(value);
}
}
}
Ensure that A2 is not empty else the script will fail.
If it is a lot of records, you can create a function and run it. The following does this until the end of the sheet, so make sure to delete all the rows towards the end which you do not need or adjust the range in the 2nd row.
function autoFillDown(){
const range = SpreadsheetApp.getActiveSheet().getRange("A:A");
const rows = range.getValues();
let outputArray = [];
rows.forEach( row => {
// if it contains a name, leave it
if( row[0].length > 1){
outputArray.push( [row[0]] )
// otherwise replace it with the value above it
} else {
outputArray.push( [outputArray[outputArray.length-1]] );
}
});
range.setValues( outputArray );
}

Add a new sheet from template into a raw google script and organize into a folder

I have this spreadsheet and I would like to copy the template in a directory for each month and add the link in each corresponding cell, adding the corresponding data
Now I do it adding new sheets but I wanted to organize by directories for each month.
It's possible?
Thanks
https://docs.google.com/spreadsheets/d/1abdggD73Zb0XmRoFaMx0ssULjtLLTHhFQV3ikbaEK_I/edit?usp=sharing
function newSheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var templateSheet = ss.getSheetByName("Plantilla")
var sheet1 = ss.getSheetByName("Sheet1")
var getNames = sheet1.getRange("G2:G").getValues().filter(String).toString().split(",");
for (var i = 0; i < getNames.length; i++) {
var copy = ss.getSheetByName(getNames[i]);
if (copy) {
Logger.log("Sheet already exists");
} else {
templateSheet.copyTo(ss).setName(getNames[i]);
ss.setActiveSheet(ss.getSheetByName(getNames[i]));
ss.moveActiveSheet(ss.getNumSheets());
}
}
}
// function to create the index
function createIndex() {
// Get all the different sheet IDs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var namesArray = sheetNamesIds(sheets);
var indexSheetNames = namesArray[0];
var indexSheetIds = namesArray[1];
// check if sheet called sheet called already exists
// if no index sheet exists, create one
if (ss.getSheetByName('index') == null) {
var indexSheet = ss.insertSheet('Index',0);
}
// if sheet called index does exist, prompt user for a different name or option to cancel
else {
var indexNewName = Browser.inputBox('The name Index is already being used, please choose a different name:', 'Please choose another name', Browser.Buttons.OK_CANCEL);
if (indexNewName != 'cancel') {
var indexSheet = ss.insertSheet(indexNewName,0);
}
else {
Browser.msgBox('No index sheet created');
}
}
// add sheet title, sheet names and hyperlink formulas
if (indexSheet) {
printIndex(indexSheet,indexSheetNames,indexSheetIds);
}
}
// function to update the index, assumes index is the first sheet in the workbook
function updateIndex() {
// Get all the different sheet IDs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var indexSheet = sheets[0];
var namesArray = sheetNamesIds(sheets);
var indexSheetNames = namesArray[0];
var indexSheetIds = namesArray[1];
printIndex(indexSheet,indexSheetNames,indexSheetIds);
}
// function to print out the index
function printIndex(sheet,names,formulas) {
sheet.clearContents();
sheet.getRange(1,1).setValue('Workbook Index').setFontWeight('bold');
sheet.getRange(3,1,names.length,1).setValues(names);
sheet.getRange(3,2,formulas.length,1).setFormulas(formulas);
}
// function to create array of sheet names and sheet ids
function sheetNamesIds(sheets) {
var indexSheetNames = [];
var indexSheetIds = [];
// create array of sheet names and sheet gids
sheets.forEach(function(sheet){
indexSheetNames.push([sheet.getSheetName()]);
indexSheetIds.push(['=hyperlink("#gid='
+ sheet.getSheetId()
+ '";"'
+ sheet.getSheetName()
+ '")']);
});
return [indexSheetNames, indexSheetIds];
}
Solution
With the use of the Drive service in Apps Script and Spreadsheet service you can easily achieve your intentions. The following is the self explanatory commented piece of code that achives what you are aiming for.
NOTE : I have only made an example of how it would be with the first month (ENERO) as to achive the rest it would just be copying what I did with the first month.
function myFunction() {
// Get the sheet where we have all the dates
var sheet = SpreadsheetApp.getActive().getSheetByName('Sheet1');
// Get the month name. Repeat this for the other months you might want to include
// For example if February is on A10 just get the range from A10
var Month1 = sheet.getRange('A1').getValue();
// Create a parent folder in the root of your drive where you will be storing everything for this project
var parentFolder = DriveApp.createFolder('Months');
// Create a folder for each month using the name we got before. Repeat for the rest of the months.
var Month1Folder = DriveApp.getFolderById(parentFolder.getId()).createFolder(Month1);
// Get the dates of each month. Repeat for the rest of the months.
// For example, if February dates go from A11 to A21 get the range A11:A21
// Also, getValues returns an array of arrays. Therefore I have flatten it to make a simple 1D array
var Month1Days = sheet.getRange('A2:A4').getValues().flat();
// For every element of our Month 1 date names array
// Repeat this block for the rest of the months substituying the array and column (to not overwrite) accordingly
for(i=0;i<Month1Days.length;i++){
// Create a new spreadsheet with the name of the dates of that month and get its id
var idFile = SpreadsheetApp.create(Month1Days[i]).getId();
// Get the file object of the new Spreadsheet
var file = DriveApp.getFileById(idFile);
// Add this file to our month folder
Month1Folder.addFile(file);
// Remove the duplicate that is still in the root folder
DriveApp.getRootFolder().removeFile(file);
// Paste a link to this new spreadsheet in our original sheet in the third column
sheet.getRange(i+1, 3).setValue(file.getUrl());
}
}
I hope this has helped you. Let me know if you need anything else or if you did not understood something. :)

Deleting sheets not found in lookup

I am trying to delete unneeded sheets from a template once relevant information has been copied across. To do this I am looking up the sheet name with a check list. If the lookup returns a value of 0.0 then I want to delete the sheet.
function myFunction() {
var studentsheet = SpreadsheetApp.openById('1Qj9T002nF6SbJRq-iINL2NisU7Ld0kSrQUkPEa6l31Q').;
var sheetsCount = studentsheet.getNumSheets();
var sheets = studentsheet.getSheets();
for (var i = 0; i < sheetsCount; i++){
var sheet = sheets[i];
var sheetName = sheet.getName();
Logger.log(sheetName);
var index = match(sheetName);
Logger.log(index);
if (index = "0.0"){
var ss = studentsheet.getSheetByName(sheetName).activate();
ss.deleteactivesheet();
}
else {}
}
function match(subject) {
var sourcesheet = SpreadsheetApp.openById('14o3ZG9gQt9RL0iti5xJllifzNiLuNxWDwTRyo-x9STI').getSheetByName("Sheet6").activate();
var lookupvalue = subject;
var lookuprange = sourcesheet.getRange(2, 2, 14, 1).getValues().map(function(d){ return d[0] });
var index = lookuprange.indexOf(subject)+1;
return index;
}
};
The problem is at the end when trying to delete the sheet. I have amended the code so it selects the sheet and makes it active but in the next line I am not allowed to call .deleteactivesheet(). Does anyone know how I can write this end part where I can select the sheet based on the index score being 0 and then delete it?
To delete a Sheet from a Spreadsheet, there are two applicable Spreadsheet class methods (as always, spelling and capitalization matter in JavaScript):
Spreadsheet#deleteSheet, which requires a Sheet object as its argument
Spreadsheet#deleteActiveSheet, which takes no arguments
The former is suitable for any type of script, and any type of trigger, while the latter only makes sense from a bound script working from a UI-based invocation (either an edit/change trigger, menu click, or other manual execution), because "activating" a sheet is a nonsense operation for a Spreadsheet resource that is not open in a UI with an attached Apps Script instance.
The minimum necessary modification is thus:
var index = match(sheet);
if (index === 0) { // if Array#indexOf returned -1 (not found), `match` returned -1 + 1 --> 0
studentsheet.deleteSheet(sheet);
}
A more pertinent modification would be something like:
function deleteNotFoundSheets() {
const studentWb = SpreadsheetApp.openById("some id");
const lookupSource = getLookupRange_(); // assumes the range to search doesn't depend on the sheets that may be deleted.
studentWb.getSheets().filter(function (s) {
return canDelete_(lookupSource, s.getName());
}).forEach(function (sheetToDelete) {
studentWb.deleteSheet(sheetToDelete);
});
}
function getLookupRange_() {
const source = SpreadsheetApp.openById("some other id");
const sheet = source.getSheetByName("some existing sheet name");
const r = sheet.getRange(...);
return r.getValues().map(...);
}
function canDelete_(lookupRange, subject) {
/** your code that returns true if the subject (the sheet name) should be deleted */
}
This modification uses available Array class methods to simplify the logic of your code (by removing iterators whose only purpose is to iterate, and instead expose the contained values to the anonymous callback functions). Basically, this code is very easily understood as "of all the sheets, we want these ones (the filter), and we want to do the same thing to them (the forEach)"
Additional Reading:
JavaScript comparison operators and this (among others) SO question
Array#filter
Array#forEach
If just like me you have been struggling to find a working example of #tehhowch above solution, here it is:
function deleteSheetsWithCriteria() {
let ss = SpreadsheetApp.getActiveSpreadsheet(),
sheetList = ss.getSheetByName('List'),
list = sheetList.getRange('A1:A'+sheetList.getLastRow()),
lookupRange = list.getValues().map(function(d){ return d[0] }) + ',List'; // List of sheets NOT to delete + lookuprange sheet
Logger.log(lookupRange)
//Delete all sheets except lookupRange
ss.getSheets().filter(function (sheet) {
return deleteCriteria_(lookupRange, sheet.getName());
}).forEach(function (sheetToDelete) {
ss.deleteSheet(sheetToDelete);
});
}
function deleteCriteria_(lookupRange, sheet) {
var index = lookupRange.indexOf(sheet);
Logger.log(index)
if (index > 0) {0} else {return index}; // change line 19 to 'return index;' only, if you want to delete the sheets in the lookupRange, rember to remove the lookupRange in variable LookupRage
}

Google Apps Script: how to copy paste ranges based on formulas?

I have a model in Google Sheets that is set up with one column per day. It contains both actuals and forecasts, and every day I need to roll forward formulas to replace forecasts with actuals. I can't roll forward the whole column, only a segment of it (there are reference numbers above and below that shouldn't be changed).
I have tried to write a script to do this for me every day, but I don't know how to make getRange reference a dynamic range. This is my attempt:
function rollColumn() {
var ss2 = SpreadsheetApp.openById('<ID redacted>');
ss2.getRange("=index(Model!$7:$7,,match(today()-2,Model!$4:$4,0)):index(Model!$168:$168,,match(today()-2,Model!$4:$4,0))").copyTo(ss2.getRange("=index(Model!$7:$7,,match(today()-1,Model!$4:$4,0)):index(Model!$168:$168,,match(today()-1,Model!$4:$4,0))"))
};
The INDEX formulas work insofar as they reference the relevant ranges (I have tested them in the spreadsheet). But clearly getRange doesn't accept formulas as an input. It also seems that Google Sheets doesn't allow for a named range to be created with formulas (which is how I would solve this in Excel).
Can someone help me recreate this functionality with GAS?
This is the closest existing question I've found on Stack Overflow, but I haven't been able to make it work:
Google Apps Script performing Index & Match function between two separate Google Sheets
Thank you!
You should add {contentsOnly:false} parameter to your code. something like this:
TemplateSheet.getRange("S2:T2").copyTo(DestSheet.getRange("S2:T"+LRow2+""), {contentsOnly:false});
Getting a date from column's title, then pasting formulas to the row to the right:
// note: we assume that sheet is disposed as in the following document: https://docs.google.com/spreadsheets/d/1BU2rhAZGOLYgzgSAdEz4fJkxEcPRpwl_TZ1SR5F0y08/edit?ts=5a32fcc5#gid=0
function find_3formulas() {
var sheet = SpreadsheetApp.getActiveSheet(),
leftTitle, // this variable will stay unused because we do not need a vertical index
topTitle = todayMinus_xDays(2),
topTitlesRange = sheet.getRange("G3:T3"),
leftTitlesRange = sheet.getRange("A4:A8"); // this range will stay unused.
var coor = findCoordinates(leftTitlesRange, leftTitle, topTitlesRange, topTitle);
if (coor.row == null || coor.column == null) {
sheet.getRange("M12:M14").setFormula('="NULL: please check logs"');
return;
}
var rowAxis = 4 + coor.row;
var colAxis = 8 + coor.column;
var fromRange = sheet.getRange(rowAxis, colAxis, 3, 1);
var toRange = sheet.getRange(rowAxis, colAxis + 1, 3, 1);
Logger.log(fromRange.getA1Notation())
Logger.log(toRange.getA1Notation());
var threeFormulas = fromRange.getFormulas();
toRange.setFormulas(threeFormulas)
}
// unused in current script!
function findCoordinates(leftTitlesRange, leftTitle, topTitlesRange, topTitle) {
var formattedDate,
row = 0,
column = 0;
if (leftTitle) {
row = findRow(leftTitlesRange, leftTitle);
}
if (topTitle) {
column = findColumn(topTitlesRange, topTitle);
}
var array = {row:row, column:column}
return array;
}
// unused in current script!
function findRow(range, valueToSearch) {
var colRows = range.getValues();
for (i = 0; i < colRows.length; i++) {
if (valueToSearch == colRows[i][0]) {return i;}
}
// however, if found nothing:
Logger.log("the value " + valueToSearch + " could not be found in row titles");
return null;
}
// assumes that column titles are dates, therefore of type object.
function findColumn(range, valueToSearch) {
var colTitles = range.getValues();
for (i = 0; i < colTitles[0].length; i++) {
if (typeof colTitles[0][i] == "object") {
formattedDate = Utilities.formatDate(colTitles[0][i], "GMT", "yyyy-MM-dd")
};
if (valueToSearch === formattedDate) {return i;}
}
// however, if found nothing:
Logger.log("today's date, " + valueToSearch + ", could not be found in column titles");
return null;
}
// substracts 2 days from today, then returns the result in string format.
function todayMinus_xDays(x) {
var d = new Date();
d = new Date(d - x * 24 * 60 * 60 * 1000);
d = Utilities.formatDate(d, "GMT", "yyyy-MM-dd");
return d;
}