Is it possible to call RichTextValueBuilder.setTextStyle( ) multiple times programmatically? - javascript

I got this these values.
And I want to have this result.
So I made the following test code and tried it to the first cell.
function test2() {
const ss = SpreadsheetApp.getActive();
const sheet = ss.getSheetByName("richText3");
const range1 = sheet.getRange("A1");
const text1 = range1.getValue();
Logger.log(text1);
const re = new RegExp(/\([ a-zA-Z\/']*\)\?/dg);
const redBold = SpreadsheetApp.newTextStyle().setBold(true).setForegroundColor('red').build();
let array;
while ((array = re.exec(text1)) !== null) {
const [start, end] = array.indices[0];
const richTxtValBlder = SpreadsheetApp.newRichTextValue()
.setText(text1)
.setTextStyle(start, end, redBold)
.build();
range1.setRichTextValue(richTxtValBlder);
}
}
After first try, I got this result.
I checked the Reference Document again and I found this comment.
setText(text) : Sets the text for this value and clears any existing text style.
When creating a new Rich Text value, this should be called before setTextStyle()
I found that I should call .setText() once and call .setTextStyle() multiple times.
But the problem is .setTextStyle() should be called programmatically according to the number of patterns in each cell and I cannot find how to do it programmatically.
Each cell may have 0 to 10 patterns and I don't want to make 10 different richTExtValueBuilder which only differ in the number of .setTextStyle() calls.
Do you have any different ideas ?

Modification points:
In your script, only cell "A1" is used, and also the 1st match is used. I thought that this might be the reason for your issue.
In order to achieve your goal, I retrieve the values from column "A". And also, I use matchAll instead of exec.
When these points are reflected in your script, how about the following modification?
Modified script:
function test2() {
const ss = SpreadsheetApp.getActive();
const sheet = ss.getSheetByName("richText3");
const range1 = sheet.getRange("A1:A" + sheet.getLastRow());
const re = new RegExp(/\([ a-zA-Z\/']*\)\?/g);
const redBold = SpreadsheetApp.newTextStyle().setBold(true).setForegroundColor('red').build();
const richTextValues = range1.getRichTextValues();
const res = richTextValues.map(([a]) => {
const array = [...a.getText().matchAll(re)];
if (array) {
const temp = a.copy();
array.forEach(a => temp.setTextStyle(a.index, a.index + a[0].length, redBold));
return [temp.build()];
}
return [a];
});
range1.setRichTextValues(res);
}
Testing:
When this script is run, the following result is obtained.
From:
To:
References:
map()
setRichTextValues(values)

It looks like you need to call .setText() once, .setTextStyle() multiple times, and .build() once, e.g. change your while loop. Untested code:
let richTxtValBlder = SpreadsheetApp.newRichTextValue().setText(text1);
while ((array = re.exec(text1)) !== null) {
const [start, end] = array.indices[0];
richTxtValBlder = richTxtValBlder.setTextStyle(start, end, redBold);
}
richTxtValBlder = richTxtValBlder.build();
range1.setRichTextValue(richTxtValBlder);

Related

Compare user input with csv file, return second column value javascript

I came across the following topic, it just has 1 line instead of 2 columns.
How do I return the second value here (see topic below)
Compare my variable with a csv file and get the matching value in javascript
This is my CSV file values:
csv screenshot of columns
This is what I have currently
IT just checks the file for the serial number from the user and marks the div with text "Valid".
This Valid should have the second Columns value.
<script>
const checkm = document.getElementById('check');
checkm.addEventListener('click', serialChecker)
async function serialChecker(){
const url = 'http://localhost/validator/serials.csv';
const response = await fetch(url);
// wait for the request to be completed
const serialdata = await response.text();
console.log(serialdata);
const inputserialnumber = document.getElementById('serialnumber').value.toString();
console.log(inputserialnumber);
// serialdata.match(/inputserialnumber/)
// serialdata.includes(inputserialnumber)
if(serialdata.includes(inputserialnumber) == true && inputserialnumber.length == 7 ){
document.getElementById('validity').innerHTML = "Valid";
startConfetti(); // from confetti.js
}else {
document.getElementById('validity').innerHTML = "Invalid";
stopConfetti(); // from confetti.js
}
//document.getElementById('test').innerHTML = "Valid";
}
</script>
This is my console output
It shows the full csv(currently), & the users input
changed the csv data into to different arrays if that helps:
array
& Thanks all in advance for taking the time to reply to my silly question!
EXTRA Clarification:
What I'm trying to do is a validate website checker.
So the user inputs their serial through an simple input field. & I have the serials in a csv file with an extra column that has the name matching to the serial.
So if the user inputs 1234567 it is present in the CSV file, my current code returns value = true for that. as it is present in the CSV file.
But I want it to return the value next to 1234567 (so in the second Column) instead, in this case "test1". So I can use that value instead of just a standard "Valid" text to be pushed back onto the website.
You can match values of two arrays by their index. In your case, I think it's easiest to use Array.map() to return a transformed array based on the one you loop trough. So for example, if you have two arrays called namesArray and valuesArray, do the following:
const validationResults = valuesArray.map((value, index) => {
return {
valid: checkValidity(value), // or whatever your validation function is called
name: namesArray[index] // match the index of the namesArray with the index of this one (valuesArray)
};
// or `return namesArray[index] + ', valid: ' + checkValidity(value)`
});
This loops through the valuesArray, and validationResults will then be an array of what you return per each item in the map function above.
One important note is that this assumes the arrays are both in the same order . If you want to sort them, for instance, do this after this.
Looking up and registering the values in a Map seems like the best answer.
// ...
const serialdata = await response.text();
const seriallookup = new Map();
// Set all Serial values to Names
for (let s in serialdata.split("\n")) {
let data = s.split(',');
seriallookup.set(data[0], data[1]);
}
Using this, checking for a serial's existance could be done with .has()
if (inputserialnumber.length == 7 && seriallookup.has(inputserialnumber)) {
And set to the elements text using
document.getElementById('validity').innerHTML = serialdata.get(inputserialnumber);
If the .csv file most likely wouldn't change between multiple requests (or if you only send just one request), you should probably initialize and request the data outside of the function.
Thank you all for the feedback.
I have not been able to use your suggestions exactly as intended.
But I managed to combine the idea's and create a new piece that does the trick for me!
const checkm = document.getElementById('check');
checkm.addEventListener('click', serialChecker)
async function serialChecker(){
const url = 'http://localhost/validator2/naamloos.csv';
const response = await fetch(url);
// wait for the request to be completed
const serialdata = await response.text();
const table = serialdata.split('\r\n');
const serialsArray = [];
const nameArray = [];
table.forEach(row =>{
const column = row.split(',');
const sArray = column[0];
const nArray = column[1];
serialsArray.push(sArray);
nameArray.push(nArray);
})
var array1 = serialsArray,
array2 = nameArray,
result = [],
i, l = Math.min(array1.length, array2.length);
for (i = 0; i < l; i++) {
result.push(array1[i], array2[i]);
}
result.push(...array1.slice(l), ...array2.slice(l));
function testfunction(array, variable){
var varindex = array.indexOf(variable)
return array[varindex+1]
}
//calling the function + userinput for serial
const inputserialnumber = document.getElementById('serialnumber').value.toString();
console.log(testfunction(result, inputserialnumber))
if(serialsArray.includes(inputserialnumber) == true && inputserialnumber.length == 7 ){
document.getElementById('validity').innerHTML = "Valid " + testfunction(result, inputserialnumber);
startConfetti();
}else {
document.getElementById('validity').innerHTML = "Invalid";
stopConfetti();
}
}
Hope this can help someone out in having an input field on their website with a .csv file in the backend (possible to have multiple for the user to select with a dropdown box with the async function).
This will check the file & will return the value from the csv that matches the serial!(based on serial number & length of the serial number(7characters))

How to load Named Ranges into array?

I have defined a couple of named ranges in my sheet following the naming schema fieldName1, fieldName2, fieldName3 etc. They are located in different places in my sheet.
I am trying to load those fieldName(s) into an array now, but I encounter an issue, as
getRange("fieldName1").getValue() is expecting a "String" of fieldName rather than a getRange(fieldName[c]).getValue() code.
Here is what I have so far:
const noFields = wsS.getRange("noFields").getValue()
var fieldName = []
for (var c = 1; c <= noFields; ++c) {
fieldName[c] = wsS.getRange(fieldName[c]).getValue()
}
noFields is a variable of how many fieldNameX named ranges I have to load, as I didn't think of a better way to load them still.
Please help, thank you.
So you want all the named ranges that start with "fieldName", and want to get all their values as an array.
You can get all the named ranges from the spreadsheet using .getActive(). However, they will not be sorted as expected.
If you simply want an array with all the values from named ranges, do:
var ss = SpreadsheetApp.getActive()
var values = ss.getNamedRanges()
.map(namedRange => namedRange.getRange().getValue())
If you only want the values from those ranges following the naming schema, do:
const PREFIX = "fieldName" // case sensitive
var values = ss.getNamedRanges()
.filter(namedRange => namedRange.getName().startsWith(PREFIX))
.map(namedRange => namedRange.getRange().getValue())
You might also want to sort the named ranges by the number after "fieldName", just in case they were entered in different order, right? But that complicates things a little since the names are strings.
One way of sorting the array is by constructing an associative array with the range names:
const PREFIX = "fieldName" // case sensitive
var ss = SpreadsheetApp.getActive()
var namedRanges = ss.getNamedRanges()
.filter(namedRange => namedRange.getName().startsWith(PREFIX))
var associativeArray = []
for (var namedRange of namedRanges) {
associativeArray[namedRange.getName().substring(PREFIX.length)] =
namedRange.getRange().getValue()
}
var values = associativeArray.flat()
Or with a custom comparator:
const PREFIX = "fieldName" // case sensitive
var ss = SpreadsheetApp.getActive()
var values = ss.getNamedRanges()
.filter(namedRange => namedRange.getName().startsWith(PREFIX))
.map(namedRange => [
Number(namedRange.getName().substring(PREFIX.length)),
namedRange.getRange().getValue() ])
.sort((itemA,itemB) => itemA[0] - itemB[0])
.map(item => item[1])
In any case, you will end up with an array of values that you can use.
Get all named ranges in to an array
SpreadsheetApp.getActive().getNamedRanges();
Class namedRanges
function namedRangesInAnArray() {
const ss = SpreadsheetApp.getActive();
const names = ['r1','r2'.....];//array of names to get
return SpreadsheetApp.getActive().getNamedRanges().filter(r => ~names.indexOf(r.getName()));
}

set a range, then concate range to a single cell on another sheet with a row above each consolidated cell

I'm trying to take the range of A5-A(last row), then on another sheet make a ##note row, and insert the concrete cell of each row below it. after the new rows have been combined and set in the second sheet, clear that range that was copied over from the first sheet.
I was looking into how to use the concrete function here because I needed a space in between the cells as they're being combined, so i thought to use this formula:
=CONCAT(" ",textjoin(" ", 1, A5:5))
but I don't know how to implement that function within google script. Also, I wanted to have a cell above it that has "## note line" so I believe that has to be looped in that code somehow as well.
here is a visual of what I am trying to do
Then put it into another sheet like this:
and this is where i got stuck on the code:
function sendRowsToOtherTab()
{
//DEFINE ALL ACTIVE SHEETS
var ss = SpreadsheetApp.getActiveSpreadsheet();
var eddGenSheet = ss.getSheetByName("Eddie Input Generator");
var lrwithcontentofEddGenSheet = eddGenSheet.getLastRow()+1;
var lcwithcontentofEddGenSheet = eddGenSheet.getLastColumn();
var sheet2= ss.getSheetByName("Combo Page");
//Current combo Variables
var totalcurrentstringRows = lrwithcontentofEddGenSheet-4;
var fullcurrentString = eddGenSheet.getRange(5,1,totalcurrentstringRows, lcwithcontentofEddGenSheet);
var cslastRow = fullcurrentString.getLastRow();
var bottomrightcolumnNumber = eddGenSheet.getRange(lrwithcontentofEddGenSheet - 1, 1).getNextDataCell(SpreadsheetApp.Direction.NEXT).getColumn();
//This is where the date needs to start being sent to
var insercellsstartingHere= sheet2.getrange("B19");
Im new to loops and coding and i don't even see a concate action
function myfunc101() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getSheetByName('Sheet1');
const tsh=ss.getSheetByName('Sheet2');
tsh.clearContents();
const rg=sh.getRange(5,1,sh.getLastRow()-4,sh.getLastRow());
const vs=rg.getDisplayValues();
vs.forEach((r,i)=>{tsh.appendRow([`## ${i+1} notes`]);tsh.appendRow(r);});
}
function myfunc102() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getSheetByName('Sheet1');
const tsh=ss.getSheetByName('Sheet2');
tsh.clearContents();
const rg=sh.getRange(5,1,sh.getLastRow()-4,sh.getLastRow());
const vs=rg.getDisplayValues();
vs.forEach((r,i)=>{tsh.appendRow([`## ${i+1} notes`]);tsh.appendRow([r.join(' ')]);});
}
This will clear B19 to bottom of sheet and then post results there
function myfunc103() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getSheetByName('Sheet1');
const tsh=ss.getSheetByName('Sheet2');
tsh.getRange(19,2,tsh.getLastRow()-18,1).clearContent();
const rg=sh.getRange(5,1,sh.getLastRow()-4,sh.getLastRow());
const vs=rg.getDisplayValues();
let oA=[];
vs.forEach((r,i)=>{oA.push([`## ${i+1} notes`]);oA.push([r.join(' ')]);});
tsh.getRange(19,2,oA.length,oA[0].length).setValues(oA);
}

Turn loop in to arrow function Javascript

I have a simple loop I want to turn into an arrow function
The function deletes all rows from the mainArray when there is a match in columns B or C with data in searchArray
removeROW_LOOP() works but removeROW_ARROW() does not
I get the error when running removeROW_ARROW() Exception: The number of columns in the data does not match the number of columns in the range. The data has 14 but the range has 21.
Thanks for any assistance with this
My data looks like
mainArray
searchArray
results
Loop (works)
function removeROW_LOOP() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
const searchArray = ss.getSheetByName('Helper_(ignore)').getDataRange().getDisplayValues().flat();
const mainArray = ss.getSheetByName('Connections').getDataRange().getDisplayValues();
let result = [];
for (var i = 0; i <= searchArray.length-1; i++) {
result = removeROW(mainArray, searchArray[i]);
}
const sht = ss.getSheetByName('AAA');
sht.clear()
sht.getRange(1,1, result.length, result[0].length).setValues(result);
}
My attempt at arrow function (does not work)
function removeROW_ARROW() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
const searchArray = ss.getSheetByName('Helper_(ignore)').getDataRange().getDisplayValues().flat();
const mainArray = ss.getSheetByName('Connections').getDataRange().getDisplayValues();
const result = searchArray.map(s => removeROW(mainArray, s));
const sht = ss.getSheetByName('AAA');
sht.clear()
sht.getRange(1,1, result.length, result[0].length).setValues(result);
}
and
function removeROW(array, item) {
array = array.filter(a => a[1]!=item && a[2]!=item);
return array
}
I don't think the problem is function vs. arrow function. Your refactor actually changes the behavior significantly.
removeROW_LOOP() will reset result upon each iteration with the newly filtered array. However, in removeROW_ARROW(), result will be an array of arrays. Specifically, each item in the result will be a filtered version of mainArray with only one searchArray entry filtered out of it.
I think you want somthing along these lines:
function removeUPDATED() {
// ...
const result = mainArray.filter(x => !searchArray.includes(x[0]) && !searchArray.includes(x[1]));
// ...
}
I might have included a bug there, but I think it demonstrates the approach you want.

forEach only posting last entry in 1d array javascript

I am trying to post all entries in a 1d array to a column in a google sheets. The array is the product of filtering two larger arrays and returning the names that do not appear on both lists.
below is an example of the generated array.
unPub = [fake name, test1, test2, test3]
Here is the code I have written so far:
function unPublished(){
const q3 = SpreadsheetApp.openById("1111111111");
const packAllergies = q3.getSheetByName("PACK_ALLERGIES");
const packSrch = packAllergies.getRange("D5:D" + packAllergies.getLastRow()).getValues().flat();
const allergyNames = allergy.getRange("A2:A" + allergy.getLastRow()).getValues().flat();
var unPub = (packSrch.filter(e => !allergyNames.includes(e)));
var sRow = allergy.getLastRow()+1
if (unPub.length > 0){
unPub.forEach(e => allergy.getRange(sRow,1).setValue(e));
}
}
I have tried a for loop to iterate over the list as well as forEach and still only get the last entry of the unPub array to post in the defined range.
How can I get each element in the array to post to the column starting at sRow?
Explanation:
You don't need a loop to set values to the sheet. In fact it is not recommended, see best practices.
You need the following two steps:
transform your row array into a column array:
unPub=unPub.map(v=>[v]);
because you want to set the data into a column.
remove the forEach loop and directly pass the values with a single line:
allergy.getRange(sRow,1,unPub.length,1).setValues(unPub);
Solution:
function unPublished(){
const q3 = SpreadsheetApp.openById("1111111111");
const packAllergies = q3.getSheetByName("PACK_ALLERGIES");
const packSrch = packAllergies.getRange("D5:D" + packAllergies.getLastRow()).getValues().flat();
const allergyNames = allergy.getRange("A2:A" + allergy.getLastRow()).getValues().flat();
var unPub = (packSrch.filter(e => !allergyNames.includes(e)));
var sRow = allergy.getLastRow()+1;
unPub=unPub.map(v=>[v]);
allergy.getRange(sRow,1,unPub.length,1).setValues(unPub);
}
Issue with your approach:
Besides performance issues which I described in the explanation section, your forEach loop does not work because you overwrite every value on the same cell. If you see, this part allergy.getRange(sRow,1) does not change in the for loop, given that sRow is constant.
If you want your approach to work, then you need to introduce an iterator i in the forEach loop and use that to iterate through the cells:
unPub.forEach((e,i) => allergy.getRange(sRow+i,1).setValue(e));
function unPublished(){
const q3 = SpreadsheetApp.openById("1111111111");
const packAllergies = q3.getSheetByName("PACK_ALLERGIES");
const packSrch = packAllergies.getRange("D5:D" + packAllergies.getLastRow()).getValues().flat();
const allergyNames = allergy.getRange("A2:A" + allergy.getLastRow()).getValues().flat();
var unPub = (packSrch.filter(e => !allergyNames.includes(e)));
var sRow = allergy.getLastRow()+1
if (unPub.length > 0){
unPub.forEach((e,i) => allergy.getRange(sRow+i,1).setValue(e));
}
}
but I really recommend you the first approach I mentioned.

Categories