How to output a hyperlink from Apps Script function into Google Sheet - google-apps-script

My first attempt was raw html, but that clearly didn't work.
I found that I'm supposed to use rich text, so I tried:
function youtubeLink(yt_id, start_stamp, end_stamp) {
const start_secs = toSecs(start_stamp)
const end_secs = toSecs(end_stamp)
const href = `https://www.youtube.com/embed/${yt_id}?start=${start_secs}&end=${end_secs}`
return (
SpreadsheetApp.newRichTextValue()
.setText("Youtube Link")
.setLinkUrl(href)
.build()
)
}
I'm calling with:
=youtubeLink(A1,A2,A3)
But that didn't work at all. The field just stayed blank.
I tried with a range, but got a circular reference. It seems like this should be easy. Not sure what I'm missing.
This works, but it is auto-formated and the link text is the same as the link:
function youtubeLink(yt_id, start_stamp, end_stamp) {
const start_secs = toSecs(start_stamp)
const end_secs = toSecs(end_stamp)
return (`https://www.youtube.com/embed/${yt_id}?start=${start_secs}&end=${end_secs}`)
}

Unfortunately, the custom function cannot directly put the RichtextValue and the built-in function to the cell. In this case, that is put as a string value. So, in this case, it is required to use a workaround. In this answer, I would like to propose the following 2 patterns.
Pattern 1:
If you want to use the functions of Spreadsheet, how about the following sample formula?
=HYPERLINK("https://www.youtube.com/embed/"&A1&"?start="&toSecs(B1)&"&end="&toSecs(C1),"Youtube Link")
In this case, the cells "A1", "B1" and "C1" are yt_id, start_stamp, end_stamp, respectively.
The function of toSecs is used from Google Apps Script.
Pattern 2:
If you want to use Google Apps Script, how about the following sample script? In this case, this script supposes that the values of yt_id, start_stamp, end_stamp are put in the cells "A1", "B1", and "C1", respectively. Please be careful about this.
function sample() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1"); // Please set the sheet name.
const [yt_id, start_stamp, end_stamp] = sheet.getRange("A1:C1").getValues()[0];
const start_secs = toSecs(start_stamp);
const end_secs = toSecs(end_stamp);
const href = `https://www.youtube.com/embed/${yt_id}?start=${start_secs}&end=${end_secs}`
const richtextValue = SpreadsheetApp.newRichTextValue().setText("Youtube Link").setLinkUrl(href).build();
sheet.getRange("D1").setRichTextValue(richtextValue);
}
When this script is run, the values of yt_id, start_stamp, end_stamp are retrieved from the cells "A1", "B1" and "C1", and the text with the hyperlink is put to the cell "D1".
Reference:
setRichTextValue(value)

Related

How to refresh google spreadsheets by clicking a "button"?

I tried writing an AppsScripts script to refresh sheet each time I press a button that the script is assigned to.
The only thing that must change each time is the cell with formula "=INDIRECT("A"&RANDBETWEEN(1;20))"
In your situation, I thought that these threads might be useful. "https://stackoverflow.com/q/56893480", "https://stackoverflow.com/q/61300428" But, in this case, the event object is used and a part of the formula is used. So, in this answer, I would like to introduce a sample script for refreshing the cells including the specific function =INDIRECT("A"&RANDBETWEEN(1;20)) by clicking a button on the Spreadsheet.
Sample script:
Please copy and paste the following script to the script editor of Spreadsheet and save the script. And, please assign the function name of myFunction to a button on the Spreadsheet. By this, when you click the button, the script is run.
function myFunction() {
const formula = '=INDIRECT("A"&RANDBETWEEN(1;20))'; // This is from your question.
const temp = formula.replace(/([=()])/g, "\\$1");
const dummyFormula = "=sample";
const obj = [
{ "from": `^${temp}|^${temp.replace(/;/g, ",")}`, "to": dummyFormula },
{ "from": `^\\${dummyFormula}`, "to": formula }
];
const sheet = SpreadsheetApp.getActiveSheet();
obj.forEach(({ from, to }) =>
sheet.createTextFinder(from).matchFormulaText(true).useRegularExpression(true).replaceAllWith(to)
);
}
When this script is run, the cells including a formula of =INDIRECT("A"&RANDBETWEEN(1;20)) in the active sheet are refreshed.
Note:
This sample script is for refreshing your provided formula of =INDIRECT("A"&RANDBETWEEN(1;20)) on the active sheet. When you change the formula, this script might be required to be modified. Please be careful about this.
Reference:
createTextFinder(findText) of Class Sheet

How to set a named range for a data validation programmatically (in Google apps script) in a Google spreadsheet?

Use Case
Example. I have a named range Apples (address "Sheet10!B2:B"), which in use for data validation for plenty of sheet cells. The data range for Apples can be changed (in a script), e.g. to "Sheet10!D2:D".
It works from UI
I can set manually a named range as a data source of data validation.
In this case, the data validation of a cell will always refer to the named range Apples with updated the data range.
How to make it in Google Apps Script?
GAS Limits
The code, for setting data validation, should look like this, if you have a namedRange object:
mySheet.getRange('F5')
.setDataValidation(
SpreadsheetApp.newDataValidation()
.requireValueInRange(
namedRange.getRange()
)
.setAllowInvalid(false)
.build()
);
DataValidationBuilder.requireValueInRange() does not work here as it requires only class Range (it cannot get NamedRange), and no reference to a named range will be used.
Is there a workaround or so?
UPD1 - Spreadsheet.getRangeByName() does not work
Getting range by name does not help, the data validation will get actual range address.
SpreadsheetApp.getActive().getRangeByName("Apples")
UPD2 No way to make it so far in GAS
As #TheMaster posted, it's not possible at this moment.
Please set +1 for posts:
https://issuetracker.google.com/issues/143913035
https://issuetracker.google.com/issues/203557342
P.S. It looks like the only solution will work is Google Sheets API.
I thought that in your situation, I thought that when Sheets API is used, your goal might be able to be used.
Workaround 1:
This workaround uses Sheets API.
Usage:
1. Prepare a Google Spreadsheet.
Please create a new Google Spreadsheet.
From Example. I have a named range Apples (address "Sheet10!B2:B"), which in use for data validation for plenty of sheet cells. The data range for Apples can be changed (in a script), e.g. to "Sheet10!D2:D"., please insert a sheet of "Sheet10" and put sample values to the cells "B2:B" and "D2:D".
Please set the named range Sheet10!B2:B as Apple.
2. Sample script.
Please copy and paste the following script to the script editor of Spreadsheet and save the script. And, please enable Sheets API at Advanced Google services.
function myFunction() {
const namedRangeName = "Apple"; // Please set the name of the named range.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Sheet10");
const requests = [{ updateCells: { range: { sheetId: sheet.getSheetId(), startRowIndex: 0, endRowIndex: 1, startColumnIndex: 0, endColumnIndex: 1 }, rows: [{ values: [{ dataValidation: { condition: { values: [{ userEnteredValue: "=" + namedRangeName }], type: "ONE_OF_RANGE" }, showCustomUi: true } }] }], fields: "dataValidation" } }];
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
}
In this request, the name of the named range is directly put to userEnteredValue.
3. Testing.
When this script is run to the above sample Spreadsheet, the following result is obtained.
When this demonstration is seen, first, you can see the named range of "Apple" which has the cells "B1:B1000". When a script is run, data validation is put to the cell "A1" with the named range of "Apple". In this case, the values of data validation indicate "B1:B1000". When the range named range "Apple" is changed from "B1:B1000" to "D1:D1000" and the data validation of "A1" is confirmed, it is found that the values are changed from "B1:B1000" to "D1:D1000".
Workaround 2:
This workaround uses the Google Spreadsheet service (SpreadsheetApp). In the current stage, it seems that the Google Spreadsheet service (SpreadsheetApp) cannot directly achieve your goal. This has already been mentioned in the discussions in the comment and TheMaster's answer. When you want to achieve this, how about checking whether the range of the named range is changed using OnChange as following workaround 2?
Usage:
1. Prepare a Google Spreadsheet.
Please create a new Google Spreadsheet.
From Example. I have a named range Apples (address "Sheet10!B2:B"), which in use for data validation for plenty of sheet cells. The data range for Apples can be changed (in a script), e.g. to "Sheet10!D2:D"., please insert a sheet of "Sheet10" and put sample values to the cells "B2:B" and "D2:D".
Please set the named range Sheet10!B2:B as Apple.
2. Sample script.
Please copy and paste the following script to the script editor of Spreadsheet and save the script. And, please install OnChange trigger to the function onChange.
First, please run createDataValidation. By this, data validation is put to the cell "A1" of "Sheet10". In this case, the set range is the range retrieved from the named range "Apple". So, in this case, the range is Sheet10!B2:B1000.
As the next step, please change the range of the named range from Sheet10!B2:B1000 to Sheet10!D2:D1000. By this, onChange` function is automatically run by the installed OnChange trigger. By this, the data validation of "A2" is updated. By this, the values of data validation are changed.
const namedRangeName = "Apple"; // Please set the name of the named range.
const datavalidationCell = "Sheet10!A2"; // As a sample. data validation is put to this cell.
function onChange(e) {
if (e.changeType != "OTHER") return;
const range = e.source.getRangeByName(namedRangeName);
const a1Notation = `'${range.getSheet().getSheetName()}'!${range.getA1Notation()}`;
const prop = PropertiesService.getScriptProperties();
const previousRange = prop.getProperty("previousRange");
if (previousRange != a1Notation) {
const rule = SpreadsheetApp.newDataValidation().requireValueInRange(e.source.getRangeByName(namedRangeName)).setAllowInvalid(false).build();
e.source.getRange(datavalidationCell).setDataValidation(rule);
}
prop.setProperty("previousRange", a1Notation);
}
// First, please run this function.
function createDataValidation() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const rule = SpreadsheetApp.newDataValidation().requireValueInRange(ss.getRangeByName(namedRangeName)).setAllowInvalid(false).build();
ss.getRange(datavalidationCell).setDataValidation(rule);
const prop = PropertiesService.getScriptProperties();
const range = ss.getRangeByName(namedRangeName);
const a1Notation = `'${range.getSheet().getSheetName()}'!${range.getA1Notation()}`;
prop.setProperty("previousRange", a1Notation);
}
References:
Method: spreadsheets.batchUpdate
UpdateCellsRequest
DataValidationRule
Currently, This seems to be impossible. This is however a known issue. +1 this feature request, if you want this implemented.
https://issuetracker.google.com/issues/143913035
Workarounds from the tracker issue creator:
If a validation rule is manually created with a NamedRange via the Sheets GUI, it can then be copied programmatically using Range.getDataValidations(), and subsequently used to programmatically create new DataValidations. DataValidations created this way maintain their connection to the NamedRange, and behave like their manually created counterparts. This demonstrates that the functionality to 'use' NamedRanges for data validation rules is already possible with Apps Scripts, but not the option to 'create' them.
As a half-answer, if you want just validation and can live without the drop-down list of valid values, you can programmatically set a custom formula that references the named range. This reference to the named range will not get expanded in the AppsScript, so future changes to the Named Range's actual range will percolate to the validator. Like so:
mySheet.getRange('F5')
.setDataValidation(
SpreadsheetApp.newDataValidation()
.requireFormulaSatisfied(
'=EQ(F5, VLOOKUP(F5, ' + namedRange.getName() + ', 1))'
)
.setAllowInvalid(false)
.build()
);
(The formula just checks that the value in the cell being tested is equal to what VLOOKUP finds for that cell, in the first column -- I'm assuming the named range content is sorted.)
Use getRangeByName()
function lfunko() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet0");
var cell = sh.getRange(1, 10);//location where datavalidation is applied
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(ss.getRangeByName("MyList")).build();
cell.setDataValidation(rule);
}

Google Slides Apps Script retrive a shape in a page

how to retrieve a particular shape base on the text of the shape.
example: I would like to retrieve a shape in which text starts with "Issue" and get the content of the text to input it in Google Sheets
thanks for helping!
I believe your goal as follows.
You want to retrieve the texts from the shapes in Google Slides.
When the top of text is Issue, you want to retrieve the text, and want to put the retrieved texts to Google Spreadsheet.
You want to achieve this using Google Apps Script.
In this case, how about the following sample script? Unfortunately, from your question, I cannot understand about the output situation you expect. So the following sample script puts the retrieved values to the 1st column.
Sample script:
Please copy and paste the following script to the script editor. And, please set the variables and run myFunction.
function myFunction() {
const presentationId = "###"; // Please set the presentation ID (Google Slides ID).
const spreadsheetId = "###"; // Please set the Spreadsheet ID.
const sheetName = "Sheet1"; // Please set the sheet name.
const searchText = "Issue";
// Check texts and retrieve texts from Google Slides.
const regex = new RegExp(`^${searchText}`);
const slides = SlidesApp.openById(presentationId).getSlides();
const values = slides.flatMap(slide => slide.getShapes().reduce((ar, shape) => {
const text = shape.getText().asString().trim();
if (regex.test(text)) ar.push([text]);
return ar;
}, []));
// Put the retrieved texts to Google Spreadsheet.
const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);
sheet.getRange(1, 1, values.length, values[0].length).setValues(values);
}
Note:
Unfortunately, I'm not sure whether above sample script is what you need. So when above script is not useful for your situation, in order to correctly understand about your goal, can you provide the sample Slides and the sample result you expect? By this, I would like to modify the script.
References:
getShapes()
getText()
setValues(values)

How to extract data from website to Google Sheet [duplicate]

I try to print historic adjusted close prices from Yahoo finance to Google Sheets.
=ImportXML("https://sg.finance.yahoo.com/quote/"&B57&"/history?p="&B57, "//tbody/tr[21]/td[6]")
Cell B57 is for example "SPY".
This works fine for historic prices up to 100 days. (it is adjusted here: tr[100])
When I try to get prices later 100 days it returns "N/A".
These prices are visible on yahoo finance.
It there a way to adjust XPATH that it works?
I noticed, that in the html code of yahoo pices about 100 days don't have this "data-reactid=1520" in the tr tag.
In the current stage, it seems that your expected values are included in the HTML data as a JSON object for Javascript. In this case, when the JSON object is retrieved with Google Apps Script, the value can be retrieved. When this is reflected in a sample Google Apps Script, how about the following sample script?
Sample script:
Please copy and paste the following script to the script editor of Google Spreadsheet and save the script. When you use this script, please put a custom function of =SAMPLE("https://sg.finance.yahoo.com/quote/SPY/history?p=SPY") to a cell. By this, the script is run.
function SAMPLE(url) {
const html = UrlFetchApp.fetch(url).getContentText().match(/root.App.main = ([\s\S\w]+?);\n/);
if (!html || html.length == 1) return "No data";
const tempObj = JSON.parse(html[1].trim());
const obj = tempObj.context.dispatcher.stores;
const header = ["date", "amount", "open", "high", "low", "close", "adjclose", "volume"];
return [header, ...obj.HistoricalPriceStore.prices
.map(o => header.map(h => {
if (h == "date") {
return new Date(o[h] * 1000)
} else if (h == "amount" && o[h]) {
return `${o[h]} ${o.type}`;
}
return o[h];
}))];
}
Testing:
When this script is run with =SAMPLE("https://sg.finance.yahoo.com/quote/SPY/history?p=SPY"), the following result is obtained.
Note:
The above script is for a custom function. If you want to use this script with the script editor, you can also the following sample script.
function myFunction() {
const url = "https://sg.finance.yahoo.com/quote/SPY/history?p=SPY"; // This URL is from your question.
const html = UrlFetchApp.fetch(url).getContentText().match(/root.App.main = ([\s\S\w]+?);\n/);
if (!html || html.length == 1) return;
const tempObj = JSON.parse(html[1].trim());
const obj = tempObj.context.dispatcher.stores;
const header = ["date", "amount", "open", "high", "low", "close", "adjclose", "volume"];
const values = [header, ...obj.HistoricalPriceStore.prices
.map(o => header.map(h => {
if (h == "date") {
return new Date(o[h] * 1000)
} else if (h == "amount" && o[h]) {
return `${o[h]} ${o.type}`;
}
return o[h];
}))];
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1"); // Please set your sheet name.
sheet.getRange(sheet.getLastRow() + 1, 1, values.length, values[0].length).setValues(values);
}
Note:
If const obj = tempObj.context.dispatcher.stores is the salted base64 data, please check this answer.
References:
Custom Functions in Google Sheets
map()
not possible because yahoo site uses JavaScript element - the infinity scroll - which kicks in after 100th value and that's the reason why you can't get past that point. you can test this by disabling JS for a given site and what's left can be scraped:
It's possible with a workaround :
Later than 100 days :
Cell with green background : the code to search
Cells with orange backgound : cells containing formulas
Cells with yellow background : data returned
Formulas used :
=IMPORTXML(A1;"substring-before(substring-after(//script[#id='fc'],'{""prices"":'),',""isPending')")
=SUBSTITUE(SUBSTITUE(SUBSTITUE(A3;"},{";"|");",";";");".";",")
=REGEXREPLACE(A4;"[a-z:{}\[\]""]+";"")
=TRANSPOSE(SPLIT(A5;"|"))
=(((C8/60)/60)/24)+DATE(1970;1;1)
IMPORTXML to import the data.
SUBSTITUE AND REGEXREPLACE to prepare the TRANSPOSE step.
TRANSPOSE to "build" the lines and SPLIT to "build" the columns.
DATE to transform timestamp to date.
Sheet
Answer:
IMPORTXML can not retrieve data which is populated by a script, and so using this formula to retrieve data from this table is not possible to do.
More Information:
As the first 100 values are loaded into the page without the use of JavaScript (as you can see by disabling JavaScript for https://sg.finance.yahoo.com/quote/SPY/history?p=SPY and reloading the page), the information can be retrieved by IMPORTXML.
As the data after the first 100 results is generated on-the-fly after scrolling down the page, the newly available data is not retrievable by IMPORTXML - as far as the formula sees, there is no 101st <tr> element and so it displays N/A: Imported content is empty .
References:
IMPORTXML - Docs Editors Help
Related Questions:
Google Sheets importXML Returns Empty Value

How to GetSheetByName using partial string in google apps script

I am trying to use .getSheetByName('') to grab a sheet whose name contain a certain string like, 'V1' or 'V4', not an exact match. Say the name of the sheet is '2020 0304 V1', the first part is always changing but it contains V1, I tried .getSheetByName('*V1') but it is not working. Any hint on how to achieve this?
Issue and workaround:
Unfortunately, in the current stage, the method like the regex cannot be used with getSheetByName(). So in this case, it is required to use the workarounds.
In this answer, I would like to propose 2 patterns.
Pattern 1:
In this pattern, test is used for searching the sheet name. getSheetName() is used for retrieving the sheet name. In this case, it supposes that the pattern is like 2020 0304 V1.
Sample script:
function myFunction() {
const searchText = "V1";
const sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
const regex = RegExp(`\\d{4} \\d{4} ${searchText}`);
const sheet = sheets.filter(s => regex.test(s.getSheetName()));
if (sheet.length > 0) console.log(sheet[0].getSheetName());
}
In this case, if there is only one sheet which has the sheet name of the pattern of \d{4} \d{4} V1 in your Spreadsheet, you can retrieve the sheet by sheet[0].
Pattern 2:
In this pattern, includes is used for searching the sheet name. getSheetName() is used for retrieving the sheet name.
Sample script:
function myFunction() {
const searchText = "V1";
const sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
const sheet = sheets.filter(s => s.getSheetName().includes(searchText));
if (sheet.length > 0) console.log(sheet[0].getSheetName());
}
In this case, if there is only one sheet which has the sheet name including V1 in your Spreadsheet, you can retrieve the sheet by sheet[0].
Note:
In this case, please enable V8 at the script editor.
References:
getSheetByName(name)
getSheetName()
test()
includes()