String Equality in Google Apps Script - javascript

I'm trying to compare a name retrieved from a JSON object, with a name as it exists on a google Sheet. Try as I might, I can't get a comparison that yields a positive.
I've tried:
IndexOf
localeCompare
==
===
I've tried
key===value
String(key)===String(value) and
String(key).valueof()=String(value).valueof.
I've also tried calling trim() on everything to make sure there are no leading/trailing white spaces (there aren't, as confirmed with a length() comparison.
As you can see from the screen shot, the values of key and value are exactly the same.
Any pointers would be gratefully received. This has held up my project for days!
Screenshot here

Description
This is not a solution but it might help find the problem. Perhaps the characters in one or the other is not what you think. Visually they compare, but what if one has a tab instead of a space. Try this, list the character codes for each and see if any character has a different value.
I've added another option that eliminates the for loop thanks to #TheMaster
Script (Option 1)
function test() {
try {
let text = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Test").getRange("A1").getValue();
console.log(text);
let code = [];
for( let i=0; i<text.length; i++ ) {
code.push(text.charCodeAt(i))
}
console.log(code.join());
}
catch(err) {
console.log(err);
}
}
Script (Option 2)
function test() {
try {
let text = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Test").getRange("A1").getValue();
console.log(text);
let code = [];
[...text].forEach( char => code.push(char.charCodeAt(0)) );
console.log(code.join());
}
catch(err) {
console.log(err);
}
}
Execution log
7:57:53 AM Notice Execution started
7:57:56 AM Info Archie White
7:57:56 AM Info 65,114,99,104,105,101,32,87,104,105,116,101
7:57:54 AM Notice Execution completed

Compare
function compare() {
const json = '{"values":[["good","bad","ugly"]]}';
const obj = JSON.parse(json);
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet0");
const [hA,...vs] = sh.getDataRange().getDisplayValues();
let matches = [];
vs.forEach((r,i) => {
r.forEach((c,j) => {
let idx = obj.values[0].indexOf(c);
if(~idx) {
matches.push({row:i+1,col:j+1,index: idx});
}
})
})
Logger.log(JSON.stringify(matches))
}
Execution log
9:31:50 AM Notice Execution started
9:31:52 AM Info [{"row":1,"col":1,"index":0},{"row":2,"col":1,"index":1},{"row":3,"col":1,"index":2}]
9:31:51 AM Notice Execution completed
Sheet1:
COL1
good
bad
ugly

Related

Deciding on whether to use Bool Check or Try/Catch when checking if String exists in Array

I have no code to share for this particular question as its more of a question of what is a better design choice performance wise.
When checking to see if a string exist in a sheet, would it be best to iterate through a column, get the values. if the value we are searching for is found make a bool true? or would it be best to encapsulate the iteration in a try block and if the string is not found; catch it as a NullReferenceException?
range = s1.getRange("A2:A").getValues()
for(var i=0;i<range.length;i++) {
if(range[i][0] === "searchableString") {
stringFound = true;
or would it be best to do
try {
for(var i=0;i<range.length;i++) {
if(range[i][0] === "searchableString") {
catch(err) {
err = console.log("string not found, move on");
}
}
}
any advice would be greatly appreciated. Thanks
Using a boolean works but I'm wondering if a try and catch logic block would be beneficial on the offchance the string we are searching for is not found; it can catch the null reference error and move to the next block of code (Whatever that may be)
The best practice is to use Array methods, like this:
const displayValues = s1.getRange('A2:A').getValues().flat();
const stringFound = displayValues.includes('searchableString');
Have you considered using the "TextFinder" Class? If what you are searching for is not found, it will only return an empty value instead of an error.
You can search on an entire Google Spreadsheet, a specific sheet, or a range. Also, you can use the method "findAll()," which will return all cells matching the search criteria in an Array or you can use the method "getCurrentMatch()" to get the current cell matching the search criteria.
Here are some samples using "findAll()"
Search the entire Google spreadsheet:
function searchEntireSheet() {
// get the active spreadsheet and creates the finder
let textFinder = SpreadsheetApp.getActive().createTextFinder("mobile").findAll();
// this is just to view the sheet and cell where the data is located
// this is just for testing and is not necessary
for (let i = 0; i < textFinder.length; i++) {
sheet = textFinder[i].getSheet().getName()
cell = textFinder[i].getA1Notation()
console.log("sheet: " + sheet + "\n" + "cells: "+ cell)
}
}
Result:
Search an Specific Sheet:
function searchSpecificSheet() {
// get the active sheet and creates the finder
let textFinder = SpreadsheetApp.getActive().getSheetByName("Test 1")
.createTextFinder("mobile").findAll();
// this is just to view the cell where the data is located
// this is just for testing and is not necessary
for (let i = 0; i < textFinder.length; i++) {
cell = textFinder[i].getA1Notation()
console.log("cells: "+ cell)
}
}
Result:
Search under an specific range:
function searchSpecificCell() {
// get the Range and creates the finder
let textFinder = SpreadsheetApp.getActive().getSheetByName("Test 1")
.getRange("B:B").createTextFinder("mobile").findAll();
// this is just to view the cell where the data is located
// this is just for testing and is not necessary
for (let i = 0; i < textFinder.length; i++) {
cell = textFinder[i].getA1Notation()
console.log("cells: "+ cell)
}
}
Result:
You can replace .findAll() with .getCurrentMatch() in any of those samples, and it will return just one cell.
Lastly, if it doesn't find the value, it will return an empty value without errors. Like this:

How to find JSON string in HTML file

I'm trying to find a plaintext JSON within a webpage, using Javascript. The JSON will appear as plaintext as seen in the browser, but it is possible that it would be truncated into separate html tags. Example:
<div>
{"kty":"RSA","e":"AQAB","n":"mZT_XuM9Lwn0j7O_YNWN_f7S_J6sLxcQuWsRVBlAM3_5S5aD0yWGV78B-Gti2MrqWwuAhb_6SkBlOvEF8-UCHR_rgZhVR1qbrxvQLE_zpamGJbFU_c1Vm8hEAvMt9ZltEGFS22BHBW079ebWI3PoDdS-DJvjjtszFdnkIZpn4oav9fzz0
</div>
<div>
xIaaxp6-qQFjKXCboun5pto59eJnn-bJl1D3LloCw7rSEYQr1x5mxhIxAFVVsNGuE9fjk0ueTDcMUbFLPYn6PopDMuN0T1B2D1Y8ClItEVbVDFb-mRPz8THJ_gexJ8C20n8m-pBlpL4WyyPuY2ScDugmfG7UnBGrDmS5w"}
</div>
I've tried to use this RegEx.
{"?\w+"?:[^}<]+(?:(?:(?:<\/[^>]+>)[^}<]*(?:<[^>]+>)+)*[^}<]*)*}
But the problem is it fails to work with nested JSON.
I may also use javascript to count the number of { and } to find where the JSON actually ends, but there must be better options than using this slow and clumsy approach.
Many thanks
Update:
Perhaps there ain't better way to do this. Below is my current code (a bit verbose but probably needed):
let regex = /{[\s\n]*"\w+"[\s\n]*:/g;
// Consider both open and close curly brackets
let brackets = /[{}]/g;
let arr0, arr;
// Try to parse every matching JSON
arr0 = match.exec(body);
if (arr0 === null) { // Nothing found
return new Promise(resolve => resolve());
}
try {
brackets.lastIndex = match.lastIndex; // After beginning of current JSON
let count = 1;
// Count for { and } to find the end of JSON.
while ((count !== 0) && ((arr = brackets.exec(body)) !== null)) {
count += (arr[0] === "{" ? 1 : -1);
}
// If nothing special, complete JSON found when count === 0;
let lastIdx = brackets.lastIndex;
let json = body.substring(match.lastIndex - arr0[0].length, lastIdx);
try {
let parsed = JSON.parse(json);
// Process the JSON here to get the original message
} catch (error) {
console.log(err);
}
...
} catch(err) {
console.log(err);
};
That's not possible in a good way, it might be possible to take a parent element's innerText and parse that:
console.log(JSON.parse(document.getElementById('outer').innerText.replace(/\s|\n/g, '')));
<div id="outer">
<div>
{"kty":"RSA","e":"AQAB","n":"mZT_XuM9Lwn0j7O_YNWN_f7S_J6sLxcQuWsRVBlAM3_5S5aD0yWGV78B-Gti2MrqWwuAhb_6SkBlOvEF8-UCHR_rgZhVR1qbrxvQLE_zpamGJbFU_c1Vm8hEAvMt9ZltEGFS22BHBW079ebWI3PoDdS-DJvjjtszFdnkIZpn4oav9fzz0
</div>
<div>
xIaaxp6-qQFjKXCboun5pto59eJnn-bJl1D3LloCw7rSEYQr1x5mxhIxAFVVsNGuE9fjk0ueTDcMUbFLPYn6PopDMuN0T1B2D1Y8ClItEVbVDFb-mRPz8THJ_gexJ8C20n8m-pBlpL4WyyPuY2ScDugmfG7UnBGrDmS5w"}
</div>
</div>
But it's likely to fail sometimes

How to create a function to count items from a set and store counts in an array parallel to one containing related items?

I am having trouble completing one of the last assignments in my semester-long high school-level programming class. I have been assigned to create a JavaScript program which counts the amount of time different ZIP codes appear in a set and output parallel arrays containing the zip codes and their counts. I am having difficulty getting the values to output. I believe that the respective zips and counts aren't being entered into their arrays at all.
I'm not looking for an original solution to the problem. I'd just like someone to tell me why my code isn't working, and possibly what I can change in my code specifically to fix it.
Usually I would never ask for help like this. I actually took the class last semester and now that I'm at the end of the year I have the option of completing it to earn college credit. I have never been the best at working with functions, and that remains true now. In the code below are all the moving parts I'm allowed to work with. I know it looks messy and rudimentary, but it's all I know. I'd appreciate it if any answers use only the sorts of things I used in my code. Another note, I am required to use functions for 'all identifiable processes', but I'm pretty sure my instructor only cares about the final product, so I'm not sure that the functions really matter, even if they could help.
var records = openZipCodeStudyRecordSet(),
uniqueZips = [],
zipCounts = [],
output = "";
function project62Part1() {
table = document.getElementById("outputTable");
function countZips(zip) {
var currentZip,
count;
while (records.readNextRecord()) {
currentZip = records.getSampleZipCode();
if (zip === currentZip) {
count++;
}
}
return count;
}
function processZip(zip) {
var currentZip;
while (records.readNextRecord()) {
currentZip = records.getSampleZipCode();
for (i = 0; i < uniqueZips.length; i++) {
if (uniqueZips[i] === "") {
uniqueZips.push(currentZip);
zipCounts[i] = countZips(currentZip);
break;
}
if (zip !== uniqueZip[i]) {
uniqueZips.push(currentZip);
zipCounts[i] = countZips(currentZip);
}
}
}
}
function createOutput(string) {
for (i = 0; i < uniqueZips.length; i++) {
string += "<tr><td>" + uniqueZips[i] + "</td><td>" + zipCounts[i] +
"</td></tr>";
}
return string;
}
processZip();
output = createOutput(output);
table.innerHTML = "<tr><td>Zip Code</td><td>Count</td></tr>" + output;
}
The output is supposed to be additional rows of zips and counts added to a table that is already set up on the page. There are no important technical errors in the code.
This is to be accomplished through the function processZip, which is meant to add respective zip and count into table rows. However, it appears as though the zip and count arrays its getting info from haven't had anything put into them by the other functions. I don't know if it is because of error in calling the functions, or what's in the functions themselves.
The HTML page this is connected to calls the function project62Part1().
That code is kind of all over the place but here's the logic you ideally want to follow:
Loop over each record in your table (outer loop) to get the zip code.
Declare an 'isFound' variable and set it to false
For each iteration of the outer loop, loop over your entire array of zip codes (inner loop).
3a. If you get a match, set isFound to true, increment your zipcode counter += 1 on the same index (since they're parallel arrays)
3b. If, at the end of your inner loop, isFound is still false, add the zipcode to your array of zip codes, and add a new array element to your zip code counters setting it to 1.
Since your zip code array and your zip code counter are parallel arrays to each other, when isFound is false, you are creating entries in both arrays, keeping them parallel to each other.
If, on 3a isFound is true, you are on the index of the zip code array that the zip code belongs to, so it should be the same index for your counter array.
In your current process zip function, the first condition will never be true, because starting out, your array size is 0 and after you start populating that array, you will never have an empty string (unless, of course, the zip code itself was an empty string)
The second if statement you have that checks if zip !== uniqueZip[i] - you are only checking that current value of uniqueZips and ignoring every other value in the array, so you will almost always have the second condition as true
I've been playing with the newer JavaScript language and syntax and your item was a good candidate for me to try out.
I did approach the code a little differently such as making the use of a Set for the unique values. Saves on code by not having to check and see if the value exists because the Set will never allow duplicate values in.
var uniqueZips = new Set();
const zipcodes = [21060, 22422, 25541, 43211, 21060, 22422, 22422, 43211, 43211, 43211];
function project62Part1() {
function processZipCodes() {
for(let index in zipcodes){
// We add every value because a SET will only allow you to add it once.
uniqueZips.add(zipcodes[index]);
}
}
// Structure our zipcode data information
function organizeZipCodeData() {
let response = {data:[]};
uniqueZips.forEach(function(zip) {
response.data.push( { 'zipcode':zip, 'appears': countZipAppearances(zip) })
});
return response;
}
function countZipAppearances(zip) {
// Default to zero even though you never expect an undefined
let count = 0;
zipcodes.forEach(function(zval) {
if (zip === zval) {
count++;
}
});
return count;
}
function showZipcodeInformation(data){
for (var index in data) {
if (data.hasOwnProperty(index)) {
var entry = [data[index]][0];
console.log(entry.zipcode, entry.appears);
}
}
}
// UI CONTENT: Construct the UI view from the data
function generateHtmlView(data){
let htmlview = "<table><tr><td>Zip Code</td><td>Count</td></tr>";
for (var index in data) {
if (data.hasOwnProperty(index)) {
var entry = [data[index]][0];
htmlview+="<tr><td>"+entry.zipcode+"</td><td>"+entry.appears+"</td></tr>";
}
}
htmlview += "</table>";
console.log(htmlview);
return htmlview;
}
// //////////////////////////////////////////////////////
// Call to gather the zipcodes
processZipCodes();
// Call to organize the zipcode data
let output = organizeZipCodeData();
// See what we have in the organized data
showZipcodeInformation(output.data);
// See what we have in the html content
generateHtmlView(output.data);
}
// Initiate the process
project62Part1();

LearnYouNode - Jugglling Async (#9) - I Must be Missing Something

There seem to be many questions about this problem out here, but none directly relate to my question AFAICT. Here is the problem statement:
This problem is the same as the previous problem (HTTP COLLECT) in that you need to use http.get(). However, this time you will be provided with three URLs as the first three command-line arguments.
You must collect the complete content provided to you by each of the URLs and print it to the console (stdout). You don't need to print out the length, just the data as a String; one line per URL. The catch is that you must print them out in the same order as the URLs are provided to you as command-line arguments.
Here is my original solution that fails:
var http = require('http')
var concat = require('concat-stream')
var args = process.argv.slice(2, 5)
var args_len = args.length
var results = []
args.forEach(function(arg, i) {
http.get(arg, function(res) {
res.setEncoding('utf8')
res.pipe(concat(function(str) {
results[i] = str
if (results.length === args_len)
results.forEach(function(val) {
console.log(val)
})
}))
}).on('error', console.error)
})
This is the solution they recommend:
var http = require('http')
var bl = require('bl')
var results = []
var count = 0
function printResults () {
for (var i = 0; i < 3; i++)
console.log(results[i])
}
function httpGet (index) {
http.get(process.argv[2 + index], function (response) {
response.pipe(bl(function (err, data) {
if (err)
return console.error(err)
results[index] = data.toString()
count++
if (count == 3)
printResults()
}))
})
}
for (var i = 0; i < 3; i++)
httpGet(i)
What I fail to grok is the fundamental difference between my code and the official solution. I am doing the same as their solution when it comes to stuffing the replies into an array to reference later. They use a counter to count the number of callbacks while I am comparing the length of two arrays (one whose length increases every callback); does that matter? When I try my solution outside the bounds of the learnyounode program it seems to work just fine. But I know that probably means little.... So someone who knows node better than I... care to explain where I have gone wrong? TIA.
They use a counter to count the number of callbacks while I am comparing the length of two arrays (one whose length increases every callback); does that matter?
Yes, it does matter. The .length of an array depends on the highest index in the array, not the actual number of assigned elements.
The difference surfaces only when the results from the asynchronous requests come back out of order. If you first assign index 0, then 1, then 2 and so on, the .length matches the number of assigned elements and would be the same as their counter. But now try out this:
var results = []
console.log(results.length) // 0 - as expected
results[1] = "lo ";
console.log(results.length) // 2 - sic!
results[0] = "Hel";
console.log(results.length) // 2 - didn't change!
results[3] = "ld!";
console.log(results.length) // 4
results[2] = "Wor";
console.log(results.length) // 4
If you would test the length after each assignment and output the array whenever you get 4, it would print
"Hello ld!"
"Hello World!"
So it turns out there were two different issues here, one of which was pointed out by #Bergi above. The two issues are as follows:
The .length method does not actually return the number of elements in the array. Rather it returns the highest index that is available. This seems quite silly. Thanks to #Bergi for pointing this out.
The scoping of the i variable is improper, and as such the value of i can change. This causes a race condition when results come back.
My final solution ended up being as follows:
var http = require('http')
var concat = require('concat-stream')
var args = process.argv.slice(2, 5)
var args_len = args.length
var results = []
var count = 0
function get_url_save(url, idx) {
http.get(url, function(res) {
res.setEncoding('utf8')
res.pipe(concat(function(str) {
results[idx] = str
if (++count === args_len)
results.forEach(function(val) {
console.log(val)
})
}))
}).on('error', console.error)
}
args.forEach(function(arg, i) {
get_url_save(arg, i)
})
Breaking the outtermost forEach into a method call solves the changing i issue since i gets passed in as parameter by value, thus never changing. The addition of the counter solves the issue described by #Bergi since the .length method isn't as intuitive as one would imagine.

Javascript if value is in array else in next array

I have found a few posts on here with similar questions but not entirely the same as what I am trying. I am currently using a simple if statement that checks the data the user enters then checks to see if it starts with a number of different values. I am doing this with the following:
var value = string;
var value = value.toLowerCase();
country = "NONE";
county = "NONE";
if (value.indexOf('ba1 ') == 0 || value.indexOf('ba2 ') == 0 || value.indexOf('ba3 ') == 0) { //CHECK AVON (MAINLAND UK) UK.AVON
country = "UK";
county = "UK.AVON";
} else if(value.indexOf('lu') == 0){//CHECK BEDFORDSHIRE (MAINLAND UK) UK.BEDS
country = "UK";
county = "UK.BEDS";
}
I have about 20-30 different if, else statements that are basically checking the post code entered and finding the county associated. However some of these if statements are incredibly long so I would like to store the values inside an array and then in the if statement simply check value.indexOf() for each of the array values.
So in the above example I would have an array as follows for the statement:
var avon = new Array('ba1 ','ba 2','ba3 ');
then inside the indexOf() use each value
Would this be possible with minimal script or am I going to need to make a function for this to work? I am ideally wanting to keep the array inside the if statement instead of querying for each array value.
You can use the some Array method (though you might need to shim it for legacy environments):
var value = string.toLowerCase(),
country = "NONE",
county = "NONE";
if (['ba1 ','ba 2','ba3 '].some(function(str) {
return value.slice(0, str.length) === str;
})) {
country = "UK";
county = "UK.AVON";
}
(using a more performant How to check if a string "StartsWith" another string? implementation also)
For an even shorter condition, you might also resort to regex (anchor and alternation):
if (/^ba(1 | 2|3 )/i.test(string)) { … }
No, it doesn’t exist, but you can make a function to do just that:
function containsAny(string, substrings) {
for(var i = 0; i < substrings.length; i++) {
if(string.indexOf(substrings[i]) !== -1) {
return true;
}
}
return false;
}
Alternatively, there’s a regular expression:
/ba[123] /.test(value)
My recomendation is to rethink your approach and use regular expressions instead of indexOf.
But if you really need it, you can use the following method:
function checkStart(value, acceptableStarts){
for (var i=0; i<acceptableStarts.length; i++) {
if (value.indexOf(acceptableStarts[i]) == 0) {
return true;
}
}
return false;
}
Your previous usage turns into:
if (checkStart(value, ['ba1', ba2 ', 'ba3'])) {
country = 'UK';
}
Even better you can generalize stuff, like this:
var countryPrefixes = {
'UK' : ['ba1','ba2 ', 'ba3'],
'FR' : ['fa2','fa2']
}
for (var key in countryPrefixes) {
if (checkStart(value, countryPrefixes[key]) {
country = key;
}
}
I'd forget using hard-coded logic for this, and just use data:
var countyMapping = {
'BA1': 'UK.AVON',
'BA2': 'UK.AVON',
'BA3': 'UK.AVON',
'LU': 'UK.BEDS',
...
};
Take successive characters off the right hand side of the postcode and do a trivial lookup in the table until you get a match. Four or so lines of code ought to do it:
function getCounty(str) {
while (str.length) {
var res = countyMapping[str];
if (res !== undefined) return res;
str = str.slice(0, -1);
}
}
I'd suggest normalising your strings first to ensure that the space between the two halves of the postcode is present and in the right place.
For extra bonus points, get the table out of a database so you don't have to modify your code when Scotland gets thrown out of leaves the UK ;-)

Categories