Saving an array globally - javascript

What I'm trying to achieve
I have a spreadsheet with 2 sheets A & B.
A has 2 columns - Name, Amount (Master List)
B has 4 columns - Name, Amount, X, Y (Transaction List)
Name column of Sheet B references Name column of Sheet A for data. Whenever a name is selected, I want to populate Amount column in B with Amount in column of sheet A as a placeholder which users can override. For this, I plan to load the Sheet A data in an array (available Globally) so that in onEdit(e) I can refer that array instead of accessing Sheet B.
But the options I could find - CacheService and PropertyService - save only string values. But I want to have:
var myGlobalArray = [];
function on init(){
//iterate and fill the array such that it has following output
//myGlobalArray[Name1] = 1
//myGlobalArray[Name2] = 2
//myGlobalArray[Name3] = 3
}
function onEdit(e){
//if selected value is name1, populate myGolbalArray[Name1] value in Amount
}
Question
Where & how to define myGlobalArray?
I tried to use cache service with JSON.Stringify and JSON.parse but the array is empty in onEdit.

Each call to your script creates a new instance of your script with its own unique globals. Every time you call a script you will actually find a global "this" for that specific instance. You are correct to look at PropertyService as a persistent way to save data.
Right off I See that your globalArray is not set up right:
var myGlobalArray = [];
needs to be
var myGlobalArray = {};
myGlobalArray['name1'] = 1
myGlobalArray['name2'] = 2
myGlobalArray['name3'] = 3
//myGlobalArray = {name3=3.0, name1=1.0, name2=2.0}
var stringArray = JSON.stringify(myGlobalArray)
//{"name1":1,"name2":2,"name3":3};
Now that can be saved to and read from the property store.
PropertiesService.getScriptProperties().setProperty("NameArray", stringArray);
stringArray = PropertiesService.getScriptProperties().getProperty("NameArray");
myGlobalArray = JSON.parse(stringArray);
Logger.log(myGlobalArray['name1']) // returns 1

It's true that CacheService and PropertyService save only string values, but you can store any scalar data by using the JSON utilities JSON.stringify() and JSON.parse().
// Save an array in cache service
CacheService.getPublicCache()
.put("myGlobalArray", JSON.stringify(myGlobalArray));
// Retrieve an array from property service
var myGlobalArray = JSON.parse( CacheService.getPublicCache()
.get("myGlobalArray") );
// Save an array in property service
PropertiesService.getDocumentProperties()
.setProperty("myGlobalArray", JSON.stringify(myGlobalArray));
// Retrieve an array from property service
var myGlobalArray = JSON.parse( PropertiesService.getDocumentProperties()
.getProperty("myGlobalArray") );
When a variable is called "Global", we are referring to its scope, saying that it is available to all code within the same module. (You can read more about scope in What is the scope of variables in JavaScript?)
But since you're looking at CacheService and PropertyService, you already know that scope is only part of the problem. Each time that onEdit() is invoked, it will be running in a new execution instance on one of Google's servers. A value that had been in a global variable in a previous instance will not be available to this new instance. Therefore, we need to populate our "global variable" in each new invocation of our script.
An elegant way to reference global variables is as names properties of the special this object. For example, every function in our script can refer to this.myGlobalArray.1
You can adapt the getRssFeed() example from the Class Cache documentation into get_myGlobalArray(), say. Then your onEdit() trigger needs only to call that first to make sure that this.myGlobalArray contains the relevant array data.
function onEdit(e){
get_myGlobalArray();
//if selected value is name1, populate myGlobalArray[Name1] value in Amount
...
sheet.getRange(e.range.getRow(),2).setValue(myGlobalArray[e.value]);
}
/**
* Ensure the global variable "myGlobalArray" is defined and contains the
* values of column A in SheetA as an array.
*/
function get_myGlobalArray() {
if (typeof this.myGlobalArray == 'undefined') {
// Global variable doesn't exist, so need to populate it
// First, check for cached value
var cache = CacheService.getPublicCache();
var cached = cache.get("myGlobalArray");
if (cached) {
// We have a cached value, so parse it and store in global
this.myGlobalArray = JSON.parse(cached);
}
else {
// No value in the cache, so load it from spreadsheet
var data = SpreadsheetApp.getActive().getSheetByName("Sheet A").getDataRange().getValues();
this.myGlobalArray = {};
for (var row=0; row<data.length; row++) {
this.myGlobalArray[data[row][0]] = data[row][6];
}
// Stringify and store the global into the cache
cache.put("myGlobalArray", JSON.stringify(this.myGlobalArray));
}
}
}
Edit: Associative Array
In the comment within onEdit(), it's indicated:
//if selected value is name1, populate myGolbalArray[Name1] value in Amount
This implies that myGlobalArray is an associative array, where the index is a non-integer value. This requirement is now reflected in the way that this.myGlobalArray gets populated when read from the spreadsheet.
for (var row=0; row<data.length; row++) {
this.myGlobalArray[data[row][0]] = data[row][6];
// ^^^^^^^^^^^^ ^^^^^^^^^^^^
// Name ---------------/ /
// Amount ------------------------/
}
Much has been written about the different flavours of Javascript arrays, for instance Javascript Associative Arrays Demystified.
1 Actually, only functions with global scope would understand this to mean "global to the script". Functions that are contained inside objects would interpret this to mean their host object only. But that's a story for another day.

Related

Dates are not being passed in from Google Scripts to JavaScript

I have a problem where my Google Script function is not passing in date values to my JavaScript. Text and numerical values however are being passed in. The Google Scripts function searches my google sheets document to find a row of values based on a value that is passed into it. It then takes the data, puts it into an array and ships it over to my JavaScript function. The JavaScript then assigns the values from the array to my HTML document.
Here is my JavaScript:
function callDataRetriever(){
var number = document.getElementById("number").value;
google.script.run.withSuccessHandler(dataRetriever).retreiveData(number);
}
function dataRetriever(data){
document.getElementById("location").value = data[0]; //This works
document.getElementById("dateOpened").value = data[1]; //This does not work. Stops the function from continuing its task.
document.getElementById("value1").value = data[2]; //Without the date everything here down works
document.getElementById("value2").value = data[3];
document.getElementById("value2").value = data[4];
document.getElementById("value4").value = data[5];
//...
}
Here is my Google Scripts:
function retreiveData(number){
var url = "urlToSpreadsheet";
var ss = SpreadsheetApp.openByUrl(url);
var ws = ss.getSheetByName("Data");
var data = ws.getRange(1,1, ws.getLastRow(), ws.getLastColumn()).getValues();
var dataValues = [];
var filterData = data.filter(
function(r){
if(r[2] == number){
var i = 3;
while(i < 29){
dataValues.push(r[i]);
i++;
}
}
}
)
return dataValues;
}
In my Logs this is how it looks:
It is grabbing the date correctly however once passed into my JavaScript the function ceases to continue.
UPDATE:
Edited code based on doubleunary's suggestion. Now getting an error that I do not fully understand:
You cannot pass a Date object but will have to serialize it before sending. From the documentation:
Legal parameters and return values are JavaScript primitives like a Number, Boolean, String, or null, as well as JavaScript objects and arrays that are composed of primitives, objects and arrays. A form element within the page is also legal as a parameter, but it must be the function’s only parameter, and it is not legal as a return value. Requests fail if you attempt to pass a Date, Function, DOM element besides a form, or other prohibited type, including prohibited types inside objects or arrays. Objects that create circular references will also fail, and undefined fields within arrays become null.
Try using Utilities.formatDate(myDate, SpreadsheetApp.getActive().getSpreadsheetTimeZone(), 'yyyy-MM-dd') in the server side and new Date(data[1]) in the client side.
It seemed all I needed to do was add document.getElementById("dateValue").valueAsDate = new Date(data[1]); in my JavaScript and leave my Google-Script alone.

Changing local variable in JavaScript affects original global with different name

I have a global declared at top of script:
var g_nutrition_summary = null;
When the user enters the page, I return network data and give this variable a value.
g_nutrition_summary = json.data;
This line is the ONLY assignment of the variable and is never called again (tested with alerts).
I later use that json.data variable to populate a Bar Chart with the plugin Chart.js. The global assignment is for later use.
Underneath the chart, the user can filter certain types of data it displays with a series of checkboxes. So my goal is, to keep an original value of what comes in from the network, and then make a LOCAL COPY of it and alter the COPY (not the global original) and repopulate the chart. Everytime the user checks/unchecks a checkbox, it will call this function and grab the ORIGINAL global (g_nutrition_summary) and re-filter that.
Here is how I do it:
function filter_nutrition_log()
{
alert("1: " + JSON.stringify(g_nutrition_summary));
// reassign object to tmp variable
var tmp_object = g_nutrition_summary;
var food_array = new Array("Grains", "Vegetables", "Fruits", "Oils");
var checked_array = new Array();
// Make an array of all choices that are checked
$(".input-range-filter").each(function()
{
var type = $(this).val();
if ($(this).is(':checked'))
{
checked_array.push(type);
}
});
alert("2: " + JSON.stringify(g_nutrition_summary));
// Loop thru all the 7 choices we chart
$.each(food_array, function(i, val)
{
// find out if choice is in array of selected checkboxes
if ($.inArray(val, checked_array) === -1)
{
// it's not, so delete it from out tmp obj we
// will use to repopulate the chart with
// (we do not want to overwrite the original array!)
delete tmp_object["datasets"][val];
}
});
// Resert graph
alert("3: " + JSON.stringify(g_nutrition_summary));
getNutritionChart(null, tmp_object, null, false);
}
Somehow, between alert "1" and alert "2". The global gets changed. Then when the user clicks a checkbox again and it calls this function, the very first alert shows that the original, global object contains the altered data to the tmp_object variable.
As you can see, I call a third party function I have created when this happens originally. Doing a search for the global there is absolutely nowhere else it is used in the instances described above.
Am I not understanding something about JavaScript variable scope?
Both objects and arrays in javascript are treated as references, so when trying to pass them to functions or to "copy" them, you are just cloning the reference
To have a "real copy", you would need to traverse the object and copy its content to another object. This can be done recursively, but fortunately jquery already comes with a function that does this: $.extend
So the solution would be:
var tmp_object = $.extend({},g_nutrition_summary);
If you have a nested object, you need to set an extra parameter:
var tmp_object = $.extend(true,{},g_nutrition_summary); // now it will do it recursively
For arrays, an easy way to make a "real copy" is, as #Branden Keck pointed out,
var arrCopy = arrOriginal.slice(0)
More on jquery extend: https://api.jquery.com/jquery.extend/
Going along with juvian's comment. To create the new array as somewhat of a "copy" and not just a reference, use:
var tmp_object= g_nutrition_summary.slice(0);
However, .slice() is only works for arrays and will not work on JSON, so to used this method you would have to create an array from the JSON
Another method that I found (although not the cleanest) suggested creating a string from the JSON and re-parsing it:
var tmp_object= JSON.parse(JSON.stringify(g_nutrition_summary));

global function and global variable in BIRT and access it from onRender of an element

I am working on BIRT crosstab report. In my report i need to create a global array variable and need to populate the array variable based on the column count of crosstab. I am calculating the columnCount in onRender event of the crosstab and and i need to pass this value to a global function and from the result of the global function need to populate the global variable. Then in some of other crosstab or the same crosstab i need to access this array variable.
Simply I am generating column names A,B,C...Z,AA,AB...etc, based on the column count generated in onRender of an data element in crosstab,i need to populate the array varible by passing this count to the global function and then the function returns the excel names for the number it received as param(like 1-A,2-B,3-C,....26-Z,27-AA...etc) this result is added to the array variable.This array variable will be used in another row of a crosstab of data element onRender, i cant do this with dataset bacause there is a sort function applied in crosstab(to overcome default sort of crosstab).
How can i achieve this, or anyother easy way to handle this?
I got it. A small careless mistake took 4 days..I solved it as follows.
Initialize the global variable and function in Report initialize event.Calling this function from a crosstab data element's onRender event with passing the counter value, the function gets this counter value and finds its equivalent excelcolumn name and insert into global variable.
columnCount = 1;
counter = 1;
names=new Array();
names.push(''); /* for 0th position */
function toName(number)
{
name = "";
while(number>0)
{
modulo = (number-1)%26;
name = String.fromCharCode(65 + modulo).toString()+name;
number = parseInt((number-modulo)/26);
}
names.push(name);
}
The above code is declared in report initialize event.
Then in a data element pass the counter value to the function as follows. and then increment the counter.
toName(columnCount);
columnCount++;
Then in a data element for which i need excel column name to set excel formula,the onRender method of this element getting the array value for the counter index.
if(counter==columnCount)
{
counter=1;
}
this.setDisplayValue(names[counter]);
counter++;
This is how the data element is changed when export to excel by onRender event.

JavaScript: global array variable returned undefined

JavaScript newbie here. I've searched and searched for answers and can't seem to figure this out. The arrays that I'm passing to a function aren't being passed as references properly. I don't think this is an async issue like many posts allude to but I could be wrong.
I have global arrays that I'm passing to a function. Inside the function, the arrays return their proper values, but when I try to access them outside of the function, they are undefined.
For context, I'm passing 3 arrays that hold the dry-bulb temperature, wet-bulb temperature, and hour that the measurements were taken for later calculations. I've only included a few sample data points for brevity. Sample code below:
function run(){
var hour = [];
var db = [];
var wb = [];
var cities = ["AB Edmonton","MI Detroit"];
getData(hour, db, wb, cities);
//this shows undefined, although within getData it is accurate data
alert(hour[1]);
}
function getData(hour, db, wb, cities){
//i= drop-down selection index, set to zero for testing
i=0;
switch(cities[i]) {
case "AB Edmonton":
hour = [1,2,3];
db = [15,18,21];
wb = [10,13,20];
break;
//case "MI Detroit":....
}
//this shows accurate values in the alert window
alert(cities[i] + " at hour:" + hour[i] + " the temp is:" + db[i]);
return [hour, db, wb];
};
run assigns empty arrays to hour, db and wb. These are variables which are locally scoped to the run function.
It then calls getData and passes those arrays as arguments.
Inside getData new local variables (also named hour, db and wb) are declared and are assigned the three empty arrays that were passed when the function was called.
The function then ignores those values and overwrites them with new arrays (these ones have contents).
It then returns another new array which holds each of those arrays.
This brings us back to run. The return value of getData is ignored completely and the original arrays (which are still stored in the hour, db and wb variables that belong to run) are accessed (but they are still empty).
You can either:
Manipulate the existing arrays inside getData instead of overwriting them. (e.g. hour = [1,2,3] may become hour.push(1); hour.push(2); hour.push(3)).
Use the return value of getData (in which case you don't need to bother assigning values or passing the empty arrays in the first place). You could use an object instead of an array so you can have useful names instead of an order here too.
Such:
function run(){
var cities = ["AB Edmonton","MI Detroit"];
var data = getData(cities);
alert(data.hour[1]);
}
function getData(cities){
//i= drop-down selection index, set to zero for testing
var i=0; // Use locally scoped variables where possible
var hour, db, wb;
switch(cities[i]) {
case "AB Edmonton":
hour = [1,2,3];
db = [15,18,21];
wb = [10,13,20];
break;
//case "MI Detroit":....
//this shows accurate values in the alert window
alert(cities[i] + " at hour:" + hour[i] + " the temp is:" + db[i]);
return { hour: hour, db: db, wb: wb];
};
Well, those aren't global variables. The one hour variable is local to run() in which it is declared with var, the other is local to getData in which it is declared as a parameter.
In your getData function you are overwriting the local variable (which initially has the value that was passed in by run()) in the line
hour = [1,2,3];
and from thereon the two variables refer to different arrays.
function getData(hour, db, wb, cities){ }
hour, db, etc are references to the initial Arrays.
When you write hour = [1,2,3];, the hour local references does not longer point to your desired Array, but to a new Array which you have just constructed: [1,2,3]. To fix this issue simply push values to the parameters
hours.push(1,2,3); so you won't overwrite your references.
This is the same problem that occurs when you do:
a = {x : 1};
function setX(obj) {
obj = {x: 2};
}
function correctSetX(obj) {
obj.x = 2;
}
The setX function will do nothing, while the correctSetX will correclty a to {x : 2}.
Thank you all for your help! I've posted how I edited my code to get it to work based on the comments. A couple things:
-I've moved all variables to be local in the getData() function. At least one of the comments gave the impression that it is better practice to keep variables local (forgive me, I am not a CSE guy by training, but I appreciate the tips and patience on your behalf)
-I wasn't able to simply use the .push method because the amount of data caused an error. (there are at least 8760 measurements per year) I can't remember the exact error but it was related to stack limits
-At the suggestion of Quentin, I instead created a dataSet object that had array properties. This object is what is returned by the getData function. Thank you again, this was a much better way to handle this
Sample below (with limited data):
function run(){
//get data
var dataSet = getData();
//test the result on the 2 hour reading
alert(dataSet.hour[1]);
}
function getData(){
//i= drop-down selection index, set to zero for testing
var i=0;
var hour,db,wb;
var cities = ["AB Edmonton","MI Detroit"];
switch(cities[i]){
case "AB Edmonton":
hour = [1,2,3];
db = [10,11,12];
wb = [13,14,15];
break;
//case "MI Detroit":...
} //end of switch
return {hour: hour, db: db, wb: wb};
}; //end of getData

How do I access this javascript object?

{"profit_center" :
{"branches":
[
{"branch": {"work_order":"1","cutover":"1","site_survey":"1","branch_number":"3310","quote":"1","configuration":"1","purchase_order":"1","hardware_swap":"1"}},
{"branch":{"work_order":"1","cutover":"1","site_survey":"1","branch_number":"3311","quote":"1","configuration":"1","purchase_order":"1","hardware_swap":"1"}},
{"branch":{"work_order":"1","cutover":"0","site_survey":"1","branch_number":"3312","quote":"1","configuration":"1","purchase_order":"1","hardware_swap":"1"}},
{"branch":{"work_order":"1","cutover":"1","site_survey":"1","branch_number":"3313","quote":"1","configuration":"1","purchase_order":"1","hardware_swap":"1"}},
{"branch":{"work_order":"1","cutover":"0","site_survey":"1","branch_number":"3314","quote":"1","configuration":"1","purchase_order":"1","hardware_swap":"1"}},
{"branch":{"work_order":"1","cutover":"1","site_survey":"1","branch_number":"3315","quote":"1","configuration":"1","purchase_order":"1","hardware_swap":"1"}}
],
"profit_center_name":"Alabama"}}
I tried accessing it in ajax through this,
data.profit_center //data here is the ajax variable e.g. function(data)
or through this data["profit_center"]
but no luck
How do I access this javascript object properly. ?
By the way that code above is from console.log(data)
EDIT:
Result from console.log(data.profit_center) and console.log(data["profit_center"]) is undefined
You can put your datain a variable like
var json = data
and you can access profit_center like
alert(json.profit_center);
alert(json.profit_center.profit_center_name); //Alabama
for(var i =0 ;i<json.profit_center.branches.length;i++){
alert(json.profit_center.branches[i]);
}
Okay I have found out why it is undefined, It is a json object so I need to parse it before i can access it like a javascript object.
var json = JSON.parse(data);
Then that's it.
First parse your data if you've not already done so.
You can access, for example, each branch_number like so:
var branches = data.profit_center.branches;
for (var i = 0, l = branches.length; i < l; i++) {
console.log(branches[i].branch.branch_number);
}
In summary, profit_center is an object and branches is an array of objects. Each element in the array contains a branch object that contains a number of keys. Loop over the branches array and for each element access the branch object inside using the key names to get the values.
The profit center name can be found by accessing the profit_center_name key on the profit_center object:
console.log(data.profit_center.profit_center_name); // Alabama
You could even use the new functional array methods to interrogate the data and pull out only those branches you need. Here I use filter to pull those objects where the purchase_order is equal to 2. Note that the numeric values in your JSON are strings, not integers.
var purchaseOrder2 = branches.filter(function (el) {
return el.branch.purchase_order === '2';
});
DEMO for the three examples

Categories

Resources