Apply text syle to a regex pattern in google sheet cells - javascript

I would like to get parts of strings (by regex) into a specific text style, but I can't manage the loop. I always get errors.
In the first row is the original text (strings separated by commas), and in the second under, is the desired text style.
Here is the sheet (french parameters) https://docs.google.com/spreadsheets/d/1vq0Ai_wEr3MamEQ-kMsXW7ZGg3RxrrkE5lITcYjO-rU/edit?usp=sharing
function NomsStyleBotanique(){
const classeur = SpreadsheetApp.getActive(); // var Feuille = classeur.getSheetByName('Feuille 1');
var ligne = classeur.getCurrentCell().getRow();
var colonne = classeur.getCurrentCell().getColumn();
var range = classeur.getActiveRange();
var richTextValues = range.getRichTextValues().map(([a]) => {
var text = a.getText();
var pos = 0;
var myregEx = /,/g;
var Noms = text.split(myregEx);
var textStyleNomPlante = SpreadsheetApp.newTextStyle()
.setFontSize(10)
.setForegroundColor("black")
.setFontFamily("Times New Roman")
.setItalic(false)
.build();
var textStyleNomAuteur = SpreadsheetApp.newTextStyle()
.setFontSize(10)
.setForegroundColor("#616399") // ("grey")
.setFontFamily("Times New Roman")
.setItalic(true)
.build();
var nbPhrases = [];
var i =0;
while (Noms){ i++; nbPhrases.push(Noms[i]); // SpreadsheetApp.getUi().alert(Noms[i]);
for (var i=0;i<nbPhrases.length;i++){
var myarr = Noms[i].split(" ");
var Espace1 = myarr[0].length+1;
var Espace2 = myarr[1].length+1;
if (Noms[i]){
if ((Noms[i].indexOf("subsp") > 1) || (Noms[i].indexOf("var.") > 1)){
var Espace3 = myarr[2].length+1;
var Espace4 = myarr[3].length+1;
pos = Espace1+Espace2+Espace3+Espace4; }
else { pos = Espace1+Espace2; } // pos = text.match(new RegExp(/\\s/, 'g'))[2];
var position = pos;
if (position > -1){
var temp = a.getTextStyle(0, position - 1);
return [
SpreadsheetApp.newRichTextValue()
.setText(Noms[i])
.setTextStyle(0, position - 1, textStyleNomPlante)
.setTextStyle(position, Noms[i].length, textStyleNomAuteur)
.build()
];
}
return [SpreadsheetApp.newRichTextValue().setText(Noms[i]).setTextStyle(Noms[i].getTextStyle()).build()];
}
}
}
} // fin boucle
);
range.setRichTextValues(richTextValues);
}

One problem here is that the author names are sometimes separated by a comma and sometimes separated by just a space. See Ten., Benth., Swart, and (Ten.) Kerguélen. However, in your comment, you said this does not happen often though and that you could just deal with this manually, so let's just assume for now that author names are never separated commas.
With the assumption, we can split the contents of each cell by , and deal with each plant name/author separately:
const plants = text.split(', ')
for (const plant of plants) {
// Find start/end of authors substring.
}
What we need is to find the indices where the "plant author" substring starts and ends.
Finding the end index of the plant author substring is easy; it's just the end of the entire plant string:
const end = plant.length
To find the start of the plant author substring, we can look for the indices of the spaces ' '. (You'll need to write your own getIndices() method for this.) If the plant contains subsp. or var., the start index is the 4th space; otherwise, it is the 2nd space:
let start
spaceIndices = getIndices(plant, ' ')
if (plant.includes('subsp.') || plant.includes('var.')) start = spaceIndices[3] + 1 // Add 1 to not include the space itself
else start = spaceIndices[1] + 1 // Add 1 to not include the space itself
Once we have the start/end indices, we can put them in an array offsets that we will use to find the startOffset and endOffset values for the setTextStyle() method.
So now we have:
const plants = text.split(', ')
let offsets = []
for (const plant of plants) {
const end = plant.length
let start
spaceIndices = getIndices(plant, ' ')
if (plant.includes('subsp.') || plant.includes('var.')) start = spaceIndices[3] + 1
else start = spaceIndices[1] + 1
offsets.push({
start,
end
})
}
Next, we have to initiate the RichTextValueBuilder object and loop through the offsets array to determine what the startOffset and endOffset values should be for the setTextStyles() method by adding where start and end values we found earlier to index
let richText = SpreadsheetApp.newRichTextValue()
.setText(text)
let authorTextStyle = SpreadsheetApp.newTextStyle()
.setBold(true)
.build()
let plantStartIndex = 0
for (const offset of offsets) {
const startOffset = plantStartIndex + offset.start
const endOffset = plantStartIndex + offset.end
richText = richText.setTextStyle(startOffset, endOffset, authorTextStyle)
plantStartIndex = plantStartIndex + offset.end + 2 // Add 2 to not include the ", " separator
}
Finally, build the RichTextValue object:
richText = richText.build()
…and tie it all together with the rest of your code:
function stylePlantNames() {
const ss = SpreadsheetApp.getActive()
const range = ss.getActiveRange()
const values = range.getValues()
let richTextValues = []
for (const row of values) {
let text = row[0]
const plants = text.split(', ')
let offsets = []
for (const plant of plants) {
const end = plant.length
let start
spaceIndices = getIndices(plant, ' ')
if (plant.includes('subsp.') || plant.includes('var.')) start = spaceIndices[3] + 1
else start = spaceIndices[1] + 1
offsets.push({
start,
end
})
}
let richText = SpreadsheetApp.newRichTextValue()
.setText(text)
let authorTextStyle = SpreadsheetApp.newTextStyle()
.setBold(true)
.build()
let plantStartIndex = 0
for (const offset of offsets) {
const startOffset = plantStartIndex + offset.start
const endOffset = plantStartIndex + offset.end
richText = richText.setTextStyle(startOffset, endOffset, authorTextStyle)
plantStartIndex = plantStartIndex + offset.end + 2
}
richText = richText.build()
richTextValues.push([richText])
}
range.setRichTextValues(richTextValues)
}
function getIndices(str, char) {
let indices = [];
for (var i = 0; i < str.length; i++) {
if (str[i] === char) indices.push(i);
}
return indices;
}
I skipped over many details of how the Apps Script APIs work for spreadsheets and rich text formatting. You'll need to set your own styles, but from your code, it seems like you already know how to do this. The tricky part of your question is figuring out how to identify the author substring, so that's what I focused on for my answer.

Related

How to increment set of 2 letters based on word occurrences in the range using GAS?

I got this one that looks hairy to me, but I'm confident you guys can crack it while having fun.
The problem:
Check of Company exists in the range
If not, get the latest ID prefix, which looks like AA, AB, etc
Generate a new prefix, which would be the following, according to item above: AC
If that company occurs more than once, then increment the ID number sufix XX001, XX002, etc.
This is what I've come up with so far:
function generateID() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const clientSheet = ss.getSheetByName('Clients');
const dataRng = clientSheet.getRange(8, 1, clientSheet.getLastRow(), clientSheet.getLastColumn());
const values = dataRng.getValues();
const companies = values.map(e => e[0]);//Gets the company for counting
for (let a = 0; a < values.length; a++) {
let company = values[a][0];
//Counts the number of occurrences of that company in the range
var companyOccurrences = companies.reduce(function (a, b) {
return a + (b == company ? 1 : 0);
}, 0);
if (companyOccurrences > 1) {
let clientIdPrefix = values[a][2].substring(0, 2);//Gets the first 2 letters of the existing company's ID
} else {
//Generate ID prefix, incrementing on the existing ID Prefixes ('AA', 'AB', 'AC'...);
let clientIdPrefix = incrementChar(values[a][2].substring(0,1));
Logger.log('Incremented Prefixes: ' + clientIdPrefix)
}
}
}
//Increment passed letter
var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
function incrementChar(c) {
var index = alphabet.indexOf(c)
if (index == -1) return -1 // or whatever error value you want
return alphabet[index + 1 % alphabet.length]
}
...and this is borrowing from tckmn's answer, which deals with one letter only.
This is the expected result:
This is the link to the file, should anyone want to give it a shot.
Thank you!
In your situation, how about the following modification?
Modified script:
// Please run this function.
function generateID() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('Clients');
const dataRng = sheet.getRange(8, 1, sheet.getLastRow() - 7, 1);
const values = dataRng.getValues();
let temp = "";
let init = "";
let count = 0;
const res = values.map(([a], i) => {
count++;
if (temp != a) {
count = 1;
temp = a;
init = i == 0 ? "AA" : wrapper(init);
}
return [`${init}${count.toString().padStart(3, "0")}`];
});
console.log(res)
sheet.getRange(8, 4, res.length, 1).setValues(res);
}
//Increment
var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
function incrementChar(c) {
var index = alphabet.indexOf(c)
if (index == -1) return -1 // or whatever error value you want
return alphabet[index + 1 % alphabet.length]
}
// I added this function.
function wrapper(str) {
const [a, b] = [...str];
const r1 = incrementChar(a);
const r2 = incrementChar(b);
return (r2 ? [a, r2] : (r1 ? [r1, "A"] : ["over"])).join("");
}
In this modification, I added a wrapper function. This wrapper function uses your showing script of incrementChar.
When this script is run to your sample Spreadsheet, console.log(res) shows [["AA001"],["AA002"],["AA003"],["AA004"],["AA005"],["AB001"],["AB002"],["AB003"],["AC001"]]. And this value is put to the column "D".
Note:
This modified sample is for your provided Spreadsheet. So please be careful this.
Reference:
map()

I am getting "undefined" while adding charcters in my array in javascript

Here is my code can someone just explain why I am getting undefined?? I have split the words and then reverse them and after reversing I tried to store them in a different array.
Thank you
const text = document.querySelector("#text");
const reverseText = document.querySelector("#reverseText");
let words = text.innerHTML.split("").reverse();
console.log(words);
let reversedWords = [];
let counter = 0;
for (var i = 0; i <= words.length - 1; i++) {
if (words[i] != " ") {
reversedWords[counter] += words[i];
} else {
counter++;
}
}
console.log(reversedWords);
<p id="text">hello nixit</p>
<p id="reverseText"></p>
Your issue is that the first time you try and access reversedWords[counter], it is undefined because you've initialised reversedWords to be an empty array.
Without changing much of your code (because you said you're doing it this way as a challenge), you could try initialising reversedWords to have the appropriate number of elements, initialised to empty strings
const content = text.textContent;
const words = content.split("").reverse();
const reversedWords = Array.from({ length: content.split(" ").length }, () => "");
You're using an array so you need to push things into it, and then join it up into a new string once the iteration is done.
const text = document.querySelector('#text');
const reverseText = document.querySelector('#reverseText');
function reverse(str) {
// `split` the string on the spaces
const words = str.split(' ');
const reversedWords = [];
for (let i = 0; i < words.length; i++) {
// For each word split, reverse, and then rejoin it
const word = words[i].split('').reverse().join('');
// And then add the word to the array
reversedWords.unshift(word);
}
// Return the completed array
return reversedWords;
}
const str = text.textContent;
reverseText.textContent = JSON.stringify(reverse(str));
<p id="text">hello nixit</p>
<p id="reverseText"></p>
Additional documentation
unshift
I suggest using a string instead of an array. Then you can split it to turn it into an array.
const reversedChars = 'hello nixit'.split("").reverse()
const reversedWords = ''
reversedChars.forEach(char => {
reversedWords += char
})
const reversedWordsArray = reversedWords.split(' ')
console.log(reversedWords)
// Output: tixin olleh
console.log(reversedWordsArray)
// Output: [ 'tixin', 'olleh' ]
Thanks to #phil and #Andy,
I am new learner who will learn and read all the use of Array.from() and than use it, but till than I have made something like this.
const text = document.querySelector("#text");
const reverseText = document.querySelector("#reverseText");
let words = text.innerHTML.split("").reverse();
let reversedWords = new Array();
let counter = 0;
let firstChar = 0;
for (var i = 0; i <= words.length - 1; i++) {
if (words[i] != " ") {
if (firstChar === 0) {
reversedWords[counter] = words[i];
firstChar++;
} else {
reversedWords[counter] += words[i];
}
} else {
firstChar = 0;
counter++;
}
}
reversedWords.forEach(word => reverseText.textContent += word + " ");
<p id="text">hello nixit
<p>
<p id="reverseText">
<p>

Look for characters and if exists increment 1

I am trying to create a folder using javascript but before I do that I need to check if the folder exists and if it does increase the number by 1.
Here's my input name: The ABC Group
I want javascript to remove ‘The’ and move it to the end and then finally add a code based on the first 4 characters followed by a number. If the first 4 characters don’t exist in the directory then it will start 01 and increment from there on.
Here's what I want to output:
I.e. ABC Group, The (ABCG01)
I am new to javascript but I have so far worked out how to do everything apart from the number part which I am stuck on.
Here's my code:
var clientName = "the ABC company";
// Change Case - START
const toCapitaliseCase = (phrase) => {
return phrase
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
let capitalise = toCapitaliseCase(clientName);
// Change Case - END
// Format Client Name if starts with 'The' - START)
if (capitalise.startsWith('The ')) {
let words = capitalise.split(' ');
let the = words[0];
let theSlice = capitalise.slice(4);
let comma = ', ';
let name = theSlice.concat('', comma, the);
let name2 = name.replace(/[^0-9a-zA-Z]/g, "");
let theSlice2 = name2.slice(0,4);
var upper = theSlice2.toUpperCase(); // output - "i am a crazy string, make me normal!"
let numbecccr = '101';
let theSlice3 = numbecccr.slice(1);
let FullCompiledName = theSlice.concat('', comma, the, ' (',upper, theSlice3, ')');
console.log(FullCompiledName);
// Format Client Name - START
}
I put your code into a function, in which I counted the number up each time the function gets called.
var clientName = "the ABC company";
function createName(clientName) {
this.number = this.number || 0;
// Change Case - START
const toCapitaliseCase = (phrase) => {
return phrase
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
let capitalise = toCapitaliseCase(clientName);
// Change Case - END
// Format Client Name if starts with 'The' - START)
if (capitalise.startsWith('The ')) {
let words = capitalise.split(' ');
let the = words[0];
let theSlice = capitalise.slice(4);
let comma = ', ';
let name = theSlice.concat('', comma, the);
let name2 = name.replace(/[^0-9a-zA-Z]/g, "");
let theSlice2 = name2.slice(0,4);
var upper = theSlice2.toUpperCase(); // output - "i am a crazy string, make me normal!"
this.number++;
let num = this.number;
if(('' + num).length == 1) {
num = '0' + num;
}
let FullCompiledName = theSlice.concat('', comma, the, ' (',upper, num, ')');
return FullCompiledName;
}
}
console.log(createName(clientName));
console.log(createName(clientName));

Google App Script Get Multiple Index Positions

I'm trying to figure out a way to get the index position of each repetition of a string.
The goal is to get each of those index positions and use the difference between each position to create a subtotal. The problem at hand is two fold. First, the distance between each string is not a standard length. Second, I seem to not be able to find out how to get multiple index positions of a particular string. I'm currently using the following code to cycle through different ranges to insert equations in to the sheet.
var Architecture = function(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var activeSheet = ss.getActiveSheet();
var data = activeSheet.getRange(18, 2, 7, activeSheet.getLastColumn()).getValues();
for (var i = 0; i < data.length; i++){
var rowData = data[i];
var checkData = data;
var row = checkData[i];
var colB = row[0];
if(colB == 'Subtotal'){
activeSheet.getRange(18 + i, 5, 1, data[0].length-4).setFormula('=iferror(sum(E' + (i+12) + ':E' + (i+17) + '))');
}else{
activeSheet.getRange(18 + i, 5).setFormula('=iferror(sum(F' + (i+18) + ':DU' + (i+18) + '))');
activeSheet.getRange(18 + i, 6)
.setFormula('=iferror(sum(filter(Invoices!$E:$E,Year(Invoices!$B:$B)=year(F$12),MONTH(Invoices!$B:$B)=Month(F$12),Invoices!$F:$F=$B' + (i+18) + ',Invoices!$A:$A=$C$2)))')
.copyTo(activeSheet.getRange('F18:DU23'));
}
}
};
var DueDiligence = function(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var activeSheet = ss.getActiveSheet();
var data = activeSheet.getRange(26, 2, 15, activeSheet.getLastColumn()).getValues();
for (var i = 0; i < data.length; i++){
var rowData = data[i];
var checkData = data;
var row = checkData[i];
var colB = row[0];
if(colB == 'Subtotal'){
activeSheet.getRange(26 + i, 5, 1, data[0].length-4).setFormula('=iferror(sum(E' + (i+12) + ':E' + (i+25) + '))');
}else{
activeSheet.getRange(26 + i, 5).setFormula('=iferror(sum(F' + (i+26) + ':DU' + (i+26) + '))');
activeSheet.getRange(26 + i, 6)
.setFormula('=iferror(sum(filter(Invoices!$E:$E,Year(Invoices!$B:$B)=year(F$12),MONTH(Invoices!$B:$B)=Month(F$12),Invoices!$F:$F=$B' + (i+26) + ',Invoices!$A:$A=$C$2)))')
.copyTo(activeSheet.getRange('F26:DU39'));
}
}
};
The goal would be to combine all these different functions into a single array that runs much quicker than the 18 seconds it currently takes to run.
EDIT
I've made some progress. Using the below function, I can get the index of each string, but it repeats that index position in the log for the number of rows that preceded it. For example, if the string is in index position two and then index position 10, the log shows 2,2,2,10,10,10,10,10,10,10,10 (only eight positions because 10-2=8).
var test = function(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var activeSheet = ss.getActiveSheet();
var data = activeSheet.getRange(14, 2, activeSheet.getLastRow()-13,1).getValues();
var newData = data.map(function(r){ return r[0]; });
//Logger.log(newData);
//Logger.log(data.indexOf("Subtotal", [i]));
for(var i = 0; i < data.length; i++){
Logger.log(newData.indexOf("Subtotal", [i]));
// }
}
};
You want to retrieve the indexes of all elements of an array called "Subtotal".
This can be done in a single line by combining map and filter:
var indexes = newData.map((element, i) => element === "Subtotal" ? i : "")
.filter(element => element !== "");
In this case, map returns an array with the matching indexes as well as empty strings (for unmatching indexes), and filter removes the empty string elements.
Edit:
If you want to retrieve an array with the differences between an index and the previous one, you can do this (the first index will be the same in this case):
var differences = indexes.map((element, i) => i == 0 ? element : element - indexes[i-1]);
Reference:
Array.prototype.map()
Array.prototype.filter()
Here's an example:
function getRowNumbersOfSameStrings() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getActiveSheet();
const rg=sh.getRange(2,1,sh.getLastRow()-1,1);
const vs=rg.getValues();
let u={};
vs.forEach(function(r,i){
if(!u.hasOwnProperty(r[0])) {
u[r[0]]=[];
u[r[0]].push(i+2);//row numbers
}else{
u[r[0]].push(i+2);//row numbers
}
});
SpreadsheetApp.getUi().showModelessDialog(HtmlService.createHtmlOutput(JSON.stringify(u)), "Results");
}
Here's my sample data:
COL1,COL2,COL3,COL4,COL5,COL6,COL7,COL8
7,2,4,0,1,1,10,4
9,3,2,5,2,6,7,6
9,2,10,10,9,7,2,11
10,7,2,10,2,6,2,0
0,2,4,0,6,3,10,0
2,7,6,7,0,3,5,8
6,9,6,11,3,5,5,8
1,4,5,5,10,0,3,11
4,3,3,3,1,9,3,2
9,4,0,9,2,8,11,2
5,8,7,0,3,6,5,4
5,3,5,3,2,3,8,3
Results:
{"0":[6],"1":[9],"2":[7],"4":[10],"5":[12,13],"6":[8],"7":[2],"9":[3,4,11],"10":[5]}

Highlighting string at multiple occurrences

I'm currently implementing a substring search. From the algorithm, I get array of substrings occurence positions where each element is in the form of [startPos, endPos].
For example (in javascript array):
[[1,3], [8,10], [15,18]]
And the string to highlight is:
ACGATCGATCGGATCGAGCGATCGAGCGATCGAT
I want to highlight (in HTML using <b>) the original string, so it will highlight or bold the string from position 1 to 3, then 8 to 10, then 15 to 18, etc (0-indexed).
A<b>CGA</b>TCGA<b>TCG</b>GATC<b>GAGC</b>GATCGAGCGATCGAT
This is what I have tried (JavaScript):
function hilightAtPositions(text, posArray) {
var startPos, endPos;
var startTag = "<b>";
var endTag = "</b>";
var hilightedText = "";
for (var i = 0; i < posArray.length; i++) {
startPos = posArray[i][0];
endPos = posArray[i][1];
hilightedText = [text.slice(0, startPos), startTag, text.slice(startPos, endPos), endTag, text.slice(endPos)].join('');
}
return hilightedText;
}
But it highlights just a range from the posArray (and I know it is still incorrect yet). So, how can I highlight a string given multiple occurrences position?
Looking at this question, and following John3136's suggestion of going from tail to head, you could do:
String.prototype.splice = function( idx, rem, s ) {
return (this.slice(0,idx) + s + this.slice(idx + Math.abs(rem)));
};
function hilightAtPositions(text, posArray) {
var startPos, endPos;
posArray = posArray.sort(function(a,b){ return a[0] - b[0];});
for (var i = posArray.length-1; i >= 0; i--) {
startPos = posArray[i][0];
endPos = posArray[i][1];
text= text.splice(endPos, 0, "</b>");
text= text.splice(startPos, 0, "<b>");
}
return text;
}
Note that in your code, you are overwriting hilightedText with each iteration, losing your changes.
Try this:
var stringToHighlight = "ACGATCGATCGGATCGAGCGATCGAGCGATCGAT";
var highlightPositions = [[1,3], [8,10], [15,18]];
var lengthDelta = 0;
for (var highlight in highlightPositions) {
var start = highlightPositions[highlight][0] + lengthDelta;
var end = highlightPositions[highlight][1] + lengthDelta;
var first = stringToHighlight.substring(0, start);
var second = stringToHighlight.substring(start, end + 1);
var third = stringToHighlight.substring(end + 1);
stringToHighlight = first + "<b>" + second + "</b>" + third;
lengthDelta += ("<b></b>").length;
}
alert(stringToHighlight);
Demo: http://jsfiddle.net/kPkk3/
Assuming that you're trying to highlight search terms or something like that. Why not replace the term with the bolding?
example:
term: abc
var text = 'abcdefgabcqq';
var term = 'abc';
text.replace(term, '<b>' + term + '</b>');
This would allow you to avoid worrying about positions, assuming that you are trying to highlight a specific string.
Assuming your list of segments is ordered from lowest start to highest, try doing your array from last to first.
That way you are not changing parts of the string you haven't reached yet.
Just change the loop to:
for (var i = posArray.length-1; i >=0; i--) {
If you want to check for multiple string matches and highlight them, this code snippet works.
function highlightMatch(text, matchString) {
let textArr = text.split(' ');
let returnArr = [];
for(let i=0; i<textArr.length; i++) {
let subStrMatch = textArr[i].toLowerCase().indexOf(matchString.toLowerCase());
if(subStrMatch !== -1) {
let subStr = textArr[i].split('');
let subStrReturn = [];
for(let j=0 ;j<subStr.length; j++) {
if(j === subStrMatch) {
subStrReturn.push('<strong>' + subStr[j]);
} else if (j === subStrMatch + (matchString.length-1)){
subStrReturn.push(subStr[j] + '<strong>');
} else {
subStrReturn.push(subStr[j]);
}
}
returnArr.push(subStrReturn.join(''));
} else {
returnArr.push(textArr[i]);
}
}
return returnArr;
}
highlightMatch('Multi Test returns multiple results', 'multi');
=> (5) ['<strong>Multi<strong>', 'Test', 'returns', '<strong>multi<strong>ple', 'results']

Categories