App Script in Google Sheets - problem with data auto complete - google-apps-script

I have two sheets. In one, the data of the manufactured devices is already entered. In the second, new warranty claims are added daily. To shorten the working time, I automatically copy the production data from the sheet Production to sheet Complaints. Currently I am solving this with VLOOKUP or INDEX function. But it works very slow when number of rows >20k.
Is it possible to write a script (using AppScripts) where after entering the ID number in the complaint sheet and selecting this cell, script will independently fill in the appropriate columns with data from 'Production'? (the order of the columns is different in both sheet)
Link to my sheet with example
I've tried all option with built-in function, I'm expecting to know and understand a possible solution using AppScript

In your situation, how about the following sample script?
Sample script:
Please copy and paste the following script to the script editor of Spreadsheet and save the script. And please confirm the sheet names again.
function myFunction() {
// Retrieve sheets.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const productionSheet = ss.getSheetByName("Production");
const complaintsSheet = ss.getSheetByName("Complaints");
// Retrieve values from sheets.
const [h1, ...v1] = productionSheet.getDataRange().getValues();
const range = complaintsSheet.getRange("A2:I" + complaintsSheet.getLastRow());
const [h2, ...v2] = range.getValues();
// Create an array for putting to Complaints sheet by converting columns.
const hconv = h2.map(h => h1.indexOf(h));
const obj = v1.reduce((o, r) => (o[r[0]] = hconv.map(i => i > -1 ? r[i] : null), o), {});
const len = Object.values(obj)[0].length;
const res = v2.map(([a]) => obj[a] || Array(len).fill(null));
// Put the created array to Complaints sheet.
range.offset(1, 0, res.length, res[0].length).setValues(res);
}
When this script is run, the values are retrieved from both sheets. And, an array is created for putting to "Complaints" sheet by checking the ID of column "A" and converting the columns. And, the array is put to "Complaints" sheet.
Note:
In this sample, I tested it using your sample Spreadsheet. If you change the spreadsheet, this script might not be able to be used. Please be careful about this.
I thought that in this case, the custom function might be able to be used. But, from your question, if the data is large, the custom function cannot be used. So, I proposed the above script.
In this sample, in order to convert the columns, the header titles are used. So, if you change the header titles, please be careful about this.
As another approach, if you want to put the value to the row when you put a value to the column "A" of "Complaints", how about the following script? In this script, the script is automatically run by a simple trigger of OnEdit. So, in this case, when you put a value to the column "A" of "Complaints" sheet, this script is automatically run, and the value is put into the edited row. Please select one of 2 scripts for your actual situation. As an important point, when you use this, please don't directly run this function because of no event object. Please be careful about this.
function onEdit(e) {
const { range, source } = e;
const complaintsSheet = range.getSheet();
if (complaintsSheet.getSheetName() != "Complaints" || range.columnStart != 1 || range.rowStart <= 2) return;
const value = range.getValue();
const productionSheet = source.getSheetByName("Production");
const [h1, ...v1] = productionSheet.getDataRange().getValues();
const [h2] = complaintsSheet.getRange("A2:I2").getValues();
const hconv = h2.map(h => h1.indexOf(h));
const obj = v1.reduce((o, r) => (o[r[0]] = hconv.map(i => i > -1 ? r[i] : null), o), {});
const len = Object.values(obj)[0].length;
const res = [obj[value] || Array(len).fill(null)];
range.offset(0, 0, res.length, res[0].length).setValues(res);
}
References:
reduce()
map()

Related

Vlookup with split text by Google Appscript

I am new to Appscript hence any help on below will be really appreciated.
My query is similar to the one posted in the below link,however, in that question the job is done by custom function and it is working bit slow and runs on every edit. In place of custom function I want to design an Appscript for the same which runs on the change of dropdown.
Link to similar question:
Google Appscript partial vlookup
Link of sample spreadsheet.
https://docs.google.com/spreadsheets/d/1vI22QCmixKe3aoWMLODTFzt7pNXIKO3pjXS4mT6GHT0/edit#gid=0
Any help on above will really be appreciated.
I believe your goal is as follows.
You want to run the script when the dropdown list of cell "A1" of "Sheet3" is changed to "Refresh".
You want to obtain the same result with your following script.
function MYLOOKUP(data1, data2) {
return data1
.map(([rollNo_Name, value]) => {
return (rollNo_Name !== '' && value === '') ?
data2.find(([rollNo,]) => rollNo_Name.split('*')[0] == rollNo)[1] :
''
});
}
In this case, how about using the OnEdit trigger of the simple trigger? When this is reflected in your sample Spreadsheet, the sample script is as follows.
Sample script:
Please copy and paste the following script to the script editor of Spreadsheet and save the script. When you run the script, please change the dropdown list of cell "A1" of "Sheet3" to "Refresh". By this, the script is run.
function onEdit(e) {
const sheetName = "Sheet3"; // This sheet name is from your Spreadsheet.
const { range, value, source } = e;
const sheet = range.getSheet();
if (sheet.getSheetName() != sheetName || range.getA1Notation() != "A1" || value != "Refresh") return;
const sheet1 = source.getSheetByName("Sheet1"); // This sheet name is from your Spreadsheet.
const sheet2 = source.getSheetByName("Sheet2"); // This sheet name is from your Spreadsheet.
const range1 = sheet1.getRange("A2:B" + sheet1.getLastRow());
const obj = sheet2.getRange("A2:B" + sheet2.getLastRow()).getValues().reduce((o, [a, b]) => (o[a] = b, o), {});
const values = range1.getValues().map(([a, b]) => {
const temp = obj[a.split("*")[0]];
return [temp && !b.toString() ? temp : null];
});
range1.offset(0, 2, values.length, 1).setValues(values);
range.setValue(null);
}
In this script, when the dropdown list of cell "A1" of "Sheet3" is changed to "Refresh", the script is run. And, the same result with your script is obtained. And, the value of the dropdown list is changed to null.
The result values are put to column "C" of "Sheet1". If you want to change this, please modify the above script.
Note:
In this script, when you directly run the function onEdit with the script editor, an error occurs. Please be careful about this.
In this script, in order to search the values, I used an object. By this, the process cost might be able to be reduced a little.
Updated: I reflected value in to be pulled only when there is no value in Column B.
References:
Simple Triggers
reduce()
map()

How to get range that has been moved?

Let's say I am getting the range A2:B2:
sheet.getRange("A2:B2").getValues();
But I added a row under A1:B1, so now my values are in A3:B3
Is it possible for Apps Script to dynamically catch that my values are now on another range ?
If not, any alternative ideas on how I can dynamically get the range of moving rows ?
I believe your goal is as follows.
You want to know whether the values of a range of "A2:B2" is moved.
In this case, how about using the named range, OnChange trigger, and PropertiesService? I thought that when those are used, your goal might be able to be achieved. When the sample script is prepared, it becomes as follows.
Usage:
1. Create a named range.
As a sample, please create a named range to the cells "A2:B2" as "sampleNamedRange1". Ref
2. Prepare sample script.
Please copy and paste the following script to the script editor of Spreadsheet. And, please install OnChange trigger to the function installedOnChange.
function installedOnChange(e) {
var originalRange = "A2:B2";
var nameOfNamedRange = "sampleNamedRange1";
if (!["INSERT_ROW", "REMOVE_ROW"].includes(e.changeType)) return;
var p = PropertiesService.getScriptProperties();
var pv = p.getProperty("range");
if (!pv) pv = originalRange;
var range = e.source.getRangeByName(nameOfNamedRange);
var a1Notation = range.getA1Notation();
if (a1Notation != pv) {
p.setProperty("range", a1Notation);
Browser.msgBox(`Range was changed from ${pv} to "${a1Notation}". Original range is "${originalRange}".`);
}
// var values = range.getValues(); // The values are not changed because of the named range.
}
3. Testing.
In this case, please do I added a row under A1:B1. By this, the script is automatically run by the OnChange trigger. And, you can see the dialog. You can see the demonstration as shown in the following image.
Note:
This is a simple sample script. So, please modify this for your actual situation.
References:
Installable Triggers
getRangeByName(name)
Properties Service
Added:
From the discussions, I understood that you wanted to retrieve the named range of the specific sheet using the name of the named range. In this case, the sample script is as follows.
When the sheet name is used
const range = SpreadsheetApp.getActive().getSheetByName("sheetName").getRange("nameOfNamedRange");
or
const range = SpreadsheetApp.getActive().getSheetByName("sheetName").getNamedRanges().find(n => n.getName() == "nameOfNamedRange").getRange();
When the active sheet is used
const range = SpreadsheetApp.getActiveSheet().getRange("nameOfNamedRange");
or
const range = SpreadsheetApp.getActiveSheet().getNamedRanges().find(n => n.getName() == "nameOfNamedRange").getRange();
This is possible through DeveloperMetadata. Metadata can be set to ranges or sheets and whenever such data(ranges or sheets) are moved, the associated metadata moves along with it as well. Unfortunately, this metadata cannot be set to arbitrary ranges, but only to single column or single row. For eg, with A2:B2, We have to set the metadata to the entirety of column A, column B and Row 2. However, once set, apps script is no more needed. Google sheets automatically keeps track of the movements of such data.
Sample script:
const setDevMetadata_ = (sheetName = 'Sheet1', rng = '2:2', key = 'a2b2') => {
SpreadsheetApp.getActive()
.getSheetByName(sheetName)
.getRange(rng)
.addDeveloperMetadata(key);
};
/**
* #description Set metadata to a specific range
* Unfortunately, this metadata cannot be set to arbitrary ranges, but only to single column or single row.
* For eg, with `A2:B2`, We have to set the metadata to the entirety of column A, column B and Row 2.
* #see https://stackoverflow.com/a/73376887
*/
const setDevMetadataToA2B2 = () => {
['2:2', 'A:A', 'B:B'].forEach((rng) => setDevMetadata_(undefined, rng));
};
/**
* #description Get a range with specific developer metadata key
*/
const getRangeWithKey = (sheetName = 'Sheet1', key = 'a2b2') => {
const sheet = SpreadsheetApp.getActive().getSheetByName(sheetName),
devFinder = sheet.createDeveloperMetadataFinder(),
[rows, columns] = ['Row', 'Column'].map((rc) =>
devFinder
.withKey(key)
.withLocationType(
SpreadsheetApp.DeveloperMetadataLocationType[rc.toUpperCase()]
)
.find()
.map((devmetadata) =>
devmetadata.getLocation()[`get${rc}`]()[`get${rc}`]()
)
);
console.log({ rows, columns });
const rng = sheet.getRange(
rows[0],
columns[0],
rows[1] ? rows[1] - rows[0] + 1 : 1,
columns[1] ? columns[1] - columns[0] + 1 : 1
);
console.log(rng.getA1Notation());
return rng;
};

Add link to entire column with appsscript

I'm retrieving information from an API and have to link the ID to the website.
I have the following code that is working but right now I'm retrieving 150 results and are expecting to have over 650. With 150 it takes two minutes to iterate through the API call and this for.
for (var i = 2; i <= count; i++) {
var forRange = sheet.getRange("A"+i)
var forValue = forRange.getValue();
const richText = SpreadsheetApp.newRichTextValue()
.setText(forValue)
.setLinkUrl('https://link.com/view.php?id=' + forValue)
.build();
forRange.setRichTextValue(richText);
}
My question is: Is there a more optimized way to do this iteration?
In your script, getValue() and setRichTextValue() are used in a loop. In this case, the process cost will become high. I think that this is the reason for your issue. In this case, how about the following modification?
Modified script:
const sheet = SpreadsheetApp.getActiveSheet(); // Please use your Sheet object here.
const range = sheet.getRange("A2:A" + sheet.getLastRow());
const values = range.getDisplayValues();
const obj = range.getRichTextValues().map(([a], i) => [a.copy().setText(values[i][0]).setLinkUrl('https://link.com/view.php?id=' + values[i][0]).build()]);
range.setRichTextValues(obj);
When this script is run, the object of RichTextValue is created for each row as an array. And, the created RichTextValue is used with setRichTextValues. By this, I thought that the process cost can be reduced.
Note:
In the current stage, getRichTextValues() cannot retrieve the number value from the cells by the current specification. Ref When I saw your sample Spreadsheet, it was found that the values of column "A" are the number values. This is the current issue. So, I modified my proposed script.
References:
setRichTextValues(values)
map()

How to update filters on dynamic data tables

I have some filters on columns D:K, and which show dynamic data depending on validation validation box as you can see in the gif below. When I choose "Active" 6 rows appear, then when I choose "Sold", more than 6 rows appear. However the new rows are out of filters range.
Problem: How can I update the filters automatically upon data validation change to include more or less rows?
Example sheet: https://docs.google.com/spreadsheets/d/1KthGhCQ0Mm2LFBk_eh_LS7jyXadVuyJpfu9sYm7SzpA/edit?usp=sharing
I believe your goal is as follows.
You want to refresh the basic filter when the dropdown list is changed and the values of columns "D" to "K" are updated.
In your situation, I thought that this sample script of this thread might be useful. But, in your situation, when the modification points of the script might be a bit complicated. So, here, I would like to propose the modified script of the sample as a sample script.
Sample script:
Please copy and paste the following script to the script editor of Google Spreadsheet, please set the sheet name and the cell A1Notation of the dropdown list, and save the script. When you run this script, please edit the dropdown list. By this, this script is automatically run.
function onEdit(e) {
const sheetName = "Sheet1"; // Please set your sheet name.
const dropDownCell = "B3"; // Please set the range of dropdown list.
const range = e.range;
const sheet = range.getSheet();
if (sheet.getSheetName() != sheetName || range.getA1Notation() != dropDownCell) return;
// Here, the existing basic filter is refreshed.
const filter = sheet.getFilter();
if (filter) {
const range = filter.getRange();
for (let i = range.getColumn(), maxCol = range.getLastColumn(); i <= maxCol; i++) {
const filterCriteria = filter.getColumnFilterCriteria(i)
if (filterCriteria) {
filter.setColumnFilterCriteria(i, filterCriteria);
}
}
}
}
About const dropDownCell = "B3";, unfortunately, from your showing sample image, I cannot understand the cell range of the dropdown list. So, please set this and save the script.
References:
Simple Triggers
Event Objects
Added:
From your provided sample Spreadsheet, when I saw it, it seems that the values are not filtered while you add the basic filter. And, when the dropdown list is changed, the basic filter is refreshed by my script. From this situation, I thought that you might have wanted to set the basic filter to the data range. If my understanding is correct, please test the following sample script.
Sample script:
function onEdit(e) {
const sheetName = "My Orders"; // Please set your sheet name.
const dropDownCell = "B4"; // Please set the range of dropdown list.
const range = e.range;
const sheet = range.getSheet();
if (sheet.getSheetName() != sheetName || range.getA1Notation() != dropDownCell) return;
// Here, the existing basic filter is refreshed.
const filter = sheet.getFilter();
if (filter) {
filter.remove();
}
const values = sheet.getRange("D3:K" + sheet.getLastRow()).getValues();
const row = values.length - [...values].reverse().findIndex(r => r.findIndex(c => c.toString() != "") > -1) + 2;
sheet.getRange("D3:K" + row).createFilter();
}
In this sample script, when you change the dropdown list, the basic filter is set to the data range of "D3:K".

AppScript to strikethrough partial text on match

I'm looking to get some help with Google Docs and Scripts. I have a workflow list that shows names of employees assigned to a task. There is also another field that indicates employees off of the day. I would like a script that can be run that would strikethrough the names of the individuals identified as off for the day. There could be multiple individuals off, so it would need to include a series of cells to reference. Results to look something like this.
[Requested outcome1
The problem I am running into is I cannot successfully find any code for even a starting point. I have seen pieces here and there, but nothing that is complete enough for me to even determine a starting point. I'm reasonably technical, but am not familiar with script writing. I have been unable to find a decent writeup on something like this so am requesting assistance if possible. Thank you!
Here is the code attempted where I am getting illegal argument on Line 27 currently. I will have it linked to a button. The individual in charge of updating the sheet daily will make all the daily changes, then once done click to button to clear any strikethrough and initiate based on new names input, if there are any.
Sample sheet link here.
https://docs.google.com/spreadsheets/d/1chSTd7Zy1qqu32qu4spSJJanwTI1SnH6rJtoMxb7iEc/edit?usp=sharing
function myFunction()
{
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('B:B').activate();
spreadsheet.getActiveRangeList().setFontLine(null);
const sheetName = "Sheet1"; // Please set the sheet name.
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
const range = sheet.getRange('B2:B')
const textsForStrikethrough = sheet.getRange("A15:A20").getValues().flat(); // Added
const modify = range.getValues().reduce((ar, e, r) => {
e.forEach((f, c) => {
textsForStrikethrough.forEach(g => {
const idx = f.indexOf(g);
if (idx > -1) ar.push({start: idx, end: idx + g.length, row: r, col: c});
});
});
return ar;
}, []);
const textStyle = SpreadsheetApp.newTextStyle().setStrikethrough(true).build();
const richTextValues = range.getRichTextValues();
modify.forEach(({start, end, row, col}) => richTextValues[row][col] = richTextValues[row][col].copy().setTextStyle(start, end, textStyle).build());
range.setRichTextValues(richTextValues);
}
I believe your goal as follows.
You want to reflect the strikethrough to the partial text in a cell using Google Apps Script as follows. (The sample image is from your question.)
In this case, I would like to propose to use RichTextValueBuilder. The sample script is as follows.
Sample script:
function myFunction() {
const textsForStrikethrough = ["John"]; // Please set the texts you want to reflect the strikethrough.
const sheetName = "Sheet1"; // Please set the sheet name.
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
const range = sheet.getDataRange();
const modify = range.getValues().reduce((ar, e, r) => {
e.forEach((f, c) => {
textsForStrikethrough.forEach(g => {
const idx = f.indexOf(g);
if (idx > -1) ar.push({start: idx, end: idx + g.length, row: r, col: c});
});
});
return ar;
}, []);
const textStyle = SpreadsheetApp.newTextStyle().setStrikethrough(true).build();
const richTextValues = range.getRichTextValues();
modify.forEach(({start, end, row, col}) => richTextValues[row][col] = richTextValues[row][col].copy().setTextStyle(start, end, textStyle).build());
range.setRichTextValues(richTextValues);
}
Result:
When above script is run, the following results are obtained.
Input situation:
This is the sample input situation before the script is run.
Output situation 1:
In this case, const textsForStrikethrough = ["John"]; is used for the input situation.
Output situation 2:
In this case, const textsForStrikethrough = ["John", "Amy"]; is used for the input situation.
Note:
In this sample script, all values are retrieved from the sheet and search the texts and reflect the strikethrough. So when you want to use this script to the specific range, please modify const range = sheet.getDataRange(); for your situation.
For example, from your sample image, when you want to use this script to the column "B", please modify it to const range = sheet.getRange("B1:B" + sheet.getLastRow());.
References:
Class RichTextValue
Class RichTextValueBuilder
Added:
About your following 2nd question,
This is perfect! Only other request I have, is how would we modify it to where it is referencing a series of other cells to lookup the names? The list of names changes daily, so looking to have all inputs be able to update by a simple change of the names on the sheet rather than modifying the code. So say the names could be input in cell A10:A15. Pulling that list of names and updating the "textsForStrikethrough" logic.
in this case, I think that at first, how about retrieving the values of cells "A10:A15", and use them as textsForStrikethrough?
Sample script 2:
function myFunction2() {
// const textsForStrikethrough = ["John"]; // Please set the texts you want to reflect the strikethrough.
const sheetName = "Sheet1"; // Please set the sheet name.
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
const range = sheet.getDataRange();
const textsForStrikethrough = sheet.getRange("A10:A15").getValues().flat(); // Added
const modify = range.getValues().reduce((ar, e, r) => {
e.forEach((f, c) => {
textsForStrikethrough.forEach(g => {
const idx = f.indexOf(g);
if (idx > -1) ar.push({start: idx, end: idx + g.length, row: r, col: c});
});
});
return ar;
}, []);
const textStyle = SpreadsheetApp.newTextStyle().setStrikethrough(true).build();
const richTextValues = range.getRichTextValues();
modify.forEach(({start, end, row, col}) => richTextValues[row][col] = richTextValues[row][col].copy().setTextStyle(start, end, textStyle).build());
range.setRichTextValues(richTextValues);
}
In this script, the values of cells "A10:A15" in the sheet of sheetName are used as textsForStrikethrough.
Note:
Unfortunately, I cannot understand about your actual situation. So when above script cannot be used for your actual situation, can you provide your sample Spreadsheet for replicating the issue? By this, I would like to confirm it.