Counting word matches - google-apps-script

This has been driving me crazy - it seems it should be simple, but I can't find how to get this to work.
In Apps Script, I have a string (taken from a cell formula) and want to count how many times a certain word or phrase appears. I thought match should do it, but I can't get it to work with a variable. Only with typing it directly. Is there a way to do this using a variable?
I have had a look at regexp, but there is not much on the Google pages. That may be my best option though I don't understand it.
function myFunction() {
var str = "'=IFERROR(IF('Train Station'!D154 ='A','A: Good - Performing well',
IF('Train Station'!D154='B','B: OK',
IF('Train Station'!D154='C','C: Do Better',
IF('Train Station'!D154='D','D: Give Up')))),'')'";
// var searchfor = '/'+'Train'+'/g';
// var regex = new RegExp(searchfor)
// var answer = RegExp(searchfor,str). //str.match(/+searchfor+/g).length;
var answer = str.match(/Train/g).length
Logger.log(answer)
}
EDIT / UPDATE
Getting closer. RegExp seems to be what I need (without all the /s\g*!!? stuff I was worried about). Now I just need to manage what happens withe the error : Cannot read property 'length' of null.
var searchfor = 'Train';
var regex = str.match(new RegExp(searchfor, "g")).length
Logger.log(regex)
I was going to progress this project using If regex == 4 then but the nulls are winning that battle. For now.
SUCCESS!!
var regex = (str.match(new RegExp(searchfor, "g"))||[]).length
I'm not going to even think how many hours I spent on this. Hopefully someone will find this helpful one day.
str is the string to look at, searchfor is the text to find, the "g" flag (global) and ||[] returns 0 if the result is null.
Thanks to all who looked at this for me.

Try it this way:
function myFunction() {
var str = "'=IFERROR(IF('Train Station'!D154 ='A','A: Good - Performing well'IF('Train Station'!D154='B','B: OK',IF('Train Station'!D154='C','C: Do Better',IF('Train Station'!D154='D','D: Give Up'),'')'";
// var searchfor = '/'+'Train'+'/g';
// var regex = new RegExp(searchfor)
// var answer = RegExp(searchfor,str). //str.match(/+searchfor+/g).length;
var answer = str.match(/Train/g).length
Logger.log(answer)
}

function myFunction() {
var str = "'=IFERROR(IF('Train Station'!D154 ='A','A: Good - Performing well',
IF('Train Station'!D154='B','B: OK',
IF('Train Station'!D154='C','C: Do Better',
IF('Train Station'!D154='D','D: Give Up')))),'')'";
var searchfor = 'Train';
var regex = (str.match(new RegExp(searchfor, "g"))||[]).length;
Logger.log(regex);
}

Related

Using Google apps script apply "go to sections based on answer" on Google Form ERROR Message : list.createChoice is not a function

I am using FORM RANGER to auto-populate data, but once it populate,Multiple choice go to sections based on answer always erase.
I am trying use GAS to keep branching while this FORM open, but ERROR message "list.createChoice is not a function".
I've read similar question before and working with this problem about 2 days but still can't figure it out......
Following is my code, wish someone can help me, thank you!
function GoToPage() {
var form = FormApp.openById('');
var list = form.getItems(FormApp.ItemType.MULTIPLE_CHOICE);
var list1 = form.getItems(FormApp.ItemType.MULTIPLE_CHOICE)[0].asMultipleChoiceItem().getChoices().map(choice => choice.getValue());
var choice1 = list1[0];
var choice2 = list1[1];
var choice3 = list1[2];
var pagelist = form.getItems(FormApp.ItemType.PAGE_BREAK);
var pagebreak01 = pagelist[2].asPageBreakItem();
var pagebreak02 = pagelist[3].asPageBreakItem();
var pagebreak03 = pagelist[4].asPageBreakItem();
var choices = [];
choices.push(list1.createChoice(choice1,pagebreak01));
choices.push(list1.createChoice(choice2,pagebreak02));
choices.push(list1.createChoice(choice3,pagebreak03));
list.setChoices(choices);
}
Addition:
I have four multiple choices, auto-populated by FORM RANGER from spreadsheet,and set four choices go to four sections one by one.
Once I execute, it shows:
「TypeError: list1.createChoice is not a function
GoToPage
# GOTOSEC.gs:16」
I thought this error might because input variables can't fit with "createChoice" function, but I read a lot of previous post and tried many times with other syntax,still can't work :(
Finally, I solved it with Rubén's solution, and substitute this kind of syntax:
choices.push(list1.createChoice(choice,pagebreak));
To this:
choices.push(list.asMultipleChoiceItem().createChoice(list1[i], pagebreak));
The problem is that the third line,
var list = form.getItems(FormApp.ItemType.MULTIPLE_CHOICE);
returns a Array. Assuming that you want to modify the first multiple choice question, replace this line by
var list = form.getItems(FormApp.ItemType.MULTIPLE_CHOICE)[0];

getValues/setValues not putting anything in target cell

I am trying to build a data logging workflow using Sheets. I've got a getValue/setValue pair that looks great, but isn't writing to the target cell, and I can't understand why. Here's the code I'm using:
function TESTcopy() {
var srcSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var srcSht = srcSpreadsheet.getSheets()[0];
var tgtSpreadsheet = SpreadsheetApp.openById('1xjjXG-tK3DIkgJTQbkR6XFhOEP5nhaNfqqXiFyMu0AY');
var tgtSht = tgtSpreadsheet.getSheets()[0];
var data = srcSht.getRange(4,2,20,18).getValues();
tgtSht.getRange(345,5,20,18).setValues(data);
}
I've gone over the entire script letter-by-letter, and have Googled and searched on SO for several hours. I know that there's a simple explanation, but I just can't see it.
Does anybody else have any ideas? Thanks in advance!

Function for word count in Google Docs Apps Script

Is there a method in Google Apps Scrips that returns the word count from a Google Document?
Lets say I'm writing a report that have a particular limit on word count. It's quite precise and it states exactly 1.8k - 2k words (yes and it's not just a single case, but many...)
In Microsoft Office Word there was a handy status bar at the bottom of the page which automatically updated the word count for me, so I tried to make one using Google Apps Scrips.
Writing a function that rips out whole text out from a current document and then calculates words again and again several times in a minute feels like a nonsense to me. It's completely inefficient and it makes CPU run for nothing but I couldn't find that function for the word count in Docs Reference.
Ctr+Shift+C opens a pop-up that contains it, which means that a function that returns total word count of a Google Document definitely exists...
But I can't find it!
Sigh... I spent few hours digging through Google, but I simply cannot find it, please help!
Wrote a little snippet that might help.
function myFunction() {
var space = " ";
var text = DocumentApp.getActiveDocument().getBody().getText();
var words = text.replace(/\s+/g, space).split(space);
Logger.log(words.length);
}
I understand the the request is for a built in function, which I looked for as well, but couldn't find anywhere in the documentation. I had to use polling.
I started with a script like Amit's, but found that I was never matching Google's word count. This is what I had to do to get it work. I know this can't be efficient, but it now matches google docs count most of the time. What I had to do was clean/rebuild the string first, then count it.
function countWords() {
var s = DocumentApp.getActiveDocument().getBody().getText();
//this function kept returning "1" when the doc was blank
//so this is how I stopped having it return 1.
if (s.length === 0)
return 0;
//A simple \n replacement didn't work, neither did \s not sure why
s = s.replace(/\r\n|\r|\n/g, " ");
//In cases where you have "...last word.First word..."
//it doesn't count the two words around the period.
//so I replace all punctuation with a space
var punctuationless = s.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()"?“”]/g," ");
//Finally, trim it down to single spaces (not sure this even matters)
var finalString = punctuationless.replace(/\s{2,}/g," ");
//Actually count it
var count = finalString.trim().split(/\s+/).length;
return count;
}
I think this function probably covers most cases for word count with English characters. If I overlooked something, please comment.
function testTheFunction(){
var myDoc = DocumentApp.openByUrl('https://docs.google.com/document/d/?????/edit');
Logger.log(countWordsInDocument(myDoc));
}
function countWordsInDocument(theDoc){
var theText = theDoc.getBody().getText();
var theRegex = new RegExp("[A-Za-z]") // or include other ranges for other languages or numbers
var wordStarted = false;
var theCount = 0;
for(var i=0;i<theText.length;i++){
var theLetter = theText.slice(i,i+1);
if(theRegex.test(theLetter)){
if(!wordStarted){
wordStarted=true;
theCount++;
}
}else if(wordStarted){
wordStarted=false;
}
}
return theCount;
}

error - "Method Range.getValue is heavily used by the script"

I posted this question previously but did not tag it properly (and hence why I likely did not get an answer) so I thought I would give it another shot as I haven't been able to find the answer in the meantime.
The below script is giving me the message in the title. I have another function which is using the same getValue method but it is running fine. What can I change in my script to avoid this issue?
function trashOldFiles() {
var ffile = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("CtrlSht").getRange("B3:B3").getValue();
var files = DriveApp.getFilesByName(ffile);
while (files.hasNext()) {
var file = files.next();
var latestfile = DriveApp.getFileById(listLatestFile());
if(file.getId() ==! latestfile){
file.setTrashed(true);
}
}
};
Is it an error or an execution hint(the light bulb in the menu)?
are you using that method on other part of your code? probably in listLatestFile()?
I got the same execution hint by calling getRange().getValue() in listLatestFile() (using a loop)
and the hint always mentioned that the problem was when calling
var ffile = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("CtrlSht").getRange("B3:B3").getValue();
in the function trashOldFiles() even when the actual problem was in other function.
Check if you are calling it in other place in your code, probably inside a loop.
OK, so Gerardo's comment about loops started to get me thinking again. I checked some other posts about how to re-use a variable and decided to put the listLatestFile() value in my spreadsheet -
var id = result[0][1];
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("CtrlSht").getRange("B5:B5").setValue(id);
//Logger.log(id);
return id;
and then retrieved the latest file ID from the spreadsheet to use as a comparison value for the trashOldFiles() function which worked a treat.
function trashOldFiles() {
var tfile = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("CtrlSht").getRange("B3:B3").getValue();
var tfiles = DriveApp.getFilesByName(tfile);
var lfile = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("CtrlSht").getRange("B5:B5").getValue();
while (tfiles.hasNext()) {
var tfile = tfiles.next();
if(tfile.getId() !== lfile){
tfile.setTrashed(true);
}
}
};
Not sure if that approach was best practice but it did work for me. If anyone has suggestions for achieving this in a more elegant way, I'm all ears.

prevent regex errors with unpredictable values

In a mail merge application I use the .replace() method to replace field identifiers by custom values and also in a reverse process to get the identifiers back.
The first way works every time since the replace first argument is a pretty normal string that I have chosen on purpose... but when I reverse the process it happens sometimes that the string contains incorrect regular expression characters.
This happens mainly on phone numbers in the form +32 2 345 345 or even with some accentuated characters.
Given I can't prevent this from happening and that I have little hope that my endusers won't use this phone number format I was wondering if someone could suggest a workaround to escape illegal characters when they come up ? note : it can be at any place in the string.
below is the code for both functions.
... (partial code)
var newField = ChampSpecial(curData,realIdx,fctSpe);// returns the value from the database
if(newField!=''){replacements.push(newField+'∏'+'#ch'+(n+1)+'#')};
//Logger.log('value in '+n+'='+realIdx+' >> '+Headers[realIdx]+' = '+ChampSpecial(curData,realIdx,fctSpe))
app.getElementById('textField'+(n+1)).setHTML(ChampSpecial(curData,realIdx,fctSpe));
if(e.parameter.source=='insertInText'){
body.replaceText('#ch'+(n+1)+'#',newField);
}
}
UserProperties.setProperty('replacements',replacements.join('|'));
cloakOn();
colorize('#ffff44');
return app;
}
function fieldsInDoc(e){
cloakOff();// remet d'abord les champs vides
var replacements = UserProperties.getProperty('replacements').split('|');
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
for(var n=0;n<replacements.length;++n){
var field = replacements[n].split('∏')[1];
var testVal = replacements[n].split('∏')[0];
body.replaceText(testVal,field);
}
colorize('#ffff44');
}
In the reverse process you are using the fieldvalues provided that can include regex special characters. you have to escape them before replacing:
body.replaceText(field.replace(/[[\]{}()*-+?.,\\^$|#\s]/, '\\$&'), '#ch'+(n+1)+'#');
This said, the "replace back the markers" a bad idea. What happens if two fields of the mail merge have the same value or the replacement text is already present in the document template...
One possible solution was to prevent the example fields in the doc from containing regex special characters so the replace had to occur in the forward process, not in the reverse (as suggested in the other answer).
Escaping these character in the fields values didn't work* so I ended up with a simple replacement by a hyphen (which make sense in most cases to replace a slash or a '+').
(*) the reverse process uses the value kept in memory so the escape sign was disturbing the replace in that function, preventing it to work properly.
the final working code goes simply like this :
//(in the first function)
var newField = ChampSpecial(curData,realIdx,fctSpe).replace(/([*+?^=!:${}()|\[\]\/\\])/g, "-");// replace every occurrence of *+?^... by '-' (global search)
About the comment stating that this approach is a bad idea I can only say that I'm afraid there is not really other ways to get that behavior and that the probability to get errors if finally quite low since the main usage of mail merge is to insert proper names, adresses, emails and phone numbers that are rarely in the template itself.
As for the field indicators they will never have the same name since they are numerically indexed (#chXX#).
EDIT : following Taras's comment I'll try another solution, will update later if it works as expected.
EDIT June 19 , Yesssss... found it.
I finally found a far better solution that doesn't use regular expression so I'm not forced to escape special characters ... the .find() method accepts any string.
The code is a bit more complex but the results is worth the pain :-))
here is the full code in 2 functions if ever someone looks for something similar.
function valuesInDoc(e){
var lock = LockService.getPrivateLock(); // just in case one clicks the second button before this one ends
var success = lock.tryLock(5000);
if (!success) {
Logger.log('tryLock failed to get the lock');
return
}
colorize('#ffffff');// this function removes the color tags on the field marlers
var app = UiApp.getActiveApplication();
var listVal = UserProperties.getProperty('listSel').split(',');
var replacements = [];
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
var find = body.findText('#ch');
if(find == null){return app };
var curData = UserProperties.getProperty('selItem').split('|');
var Headers = [];
var OriHeaders = UserProperties.getProperty('Headers').split('|');
for(n=0;n<OriHeaders.length;++n){
Headers.push('#'+OriHeaders[n]+'#');
}
var fctSpe = 0 ;
for(var i in Headers){if(Headers[i].indexOf('SS')>-1){fctSpe = i}}
for(var n=0;n<listVal.length;++n){
var realIdx = Number(listVal[n]);
Logger.log(n);
var newField = ChampSpecial(curData,realIdx,fctSpe);
//Logger.log(newField);
app.getElementById('textField'+(n+1)).setHTML(ChampSpecial(curData,realIdx,fctSpe));
if(e.parameter.source=='insertInText'){
var found = body.findText('#ch'+(n+1)+'#');// look for every field markers in the whole doc
while(found!=null){
var elemTxt = found.getElement().asText();
var startOffset = found.getStartOffset();
var len = ('#ch'+(n+1)+'#').length;
elemTxt.deleteText(startOffset, found.getEndOffsetInclusive())
elemTxt.insertText(startOffset,newField);// remove the marker and write the sample value in place
Logger.log('n='+n+' newField = '+newField+' for '+'#ch'+(n+1)+'#'+' at position '+startOffset)
replacements.push(newField+'∏'+'#ch'+(n+1)+'#'+'∏'+startOffset);// memorize the change that just occured
found = body.findText('#ch'+(n+1)+'#',found); //loop until all markers are replaced
}
}
}
UserProperties.setProperty('replacements',replacements.join('|'));
cloakOn();
colorize('#ffff44');// colorize the markers if ever one is left but it shouldn't happen
lock.releaseLock();
return app;
}
function fieldsInDoc(e){
var lock = LockService.getPrivateLock();
var success = lock.tryLock(5000);
if (!success) {
Logger.log('tryLock failed to get the lock');
return
}
cloakOff();// remet d'abord les champs vides > shows the hidden fields (markers that had no sample velue in the first function
var replacements = UserProperties.getProperty('replacements').split('|');// recover replacement data as an array
Logger.log(replacements)
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
for(var n=replacements.length-1;n>=0;n--){ // for each replacement find the data in doc and write a field marker in place
var testVal = replacements[n].split('∏')[0]; // [0] is the sample value
if(body.findText(testVal)==null){break};// this is only to handle the case one click on the wrong button trying to place markers again when they are already there ;-)
var field = replacements[n].split('∏')[1];
var testValLength = testVal.length;
var found = body.findText(testVal);
var startOffset = found.getStartOffset();
Logger.log(testVal+' = '+field+' / start: '+startOffset+' / Length: '+ testValLength)
var elemTxt = found.getElement().asText();
elemTxt.deleteText(startOffset, startOffset+testValLength-1);// remove the text
// elemTxt.deleteText(startOffset, found.getEndOffsetInclusive() )
elemTxt.insertText(startOffset,field);// and write the marker
}
colorize('#ffff44'); // colorize the marker
lock.releaseLock();// and release the lock
}