Google AppScript - Merge arrays based on key - javascript

I've looked through a lot of similar answers to this question but have not gotten any solutions to work.
I have two separate functions with one array each, that I populate like this:
folderMap.push([{siteurl: filename,logid: fileurl}]);
mapping.push([{siteurl: x[0],shareddriveid: x[1]}]);
I want to have a third function that I want to match the items based on siteurl and put that object in one array while putting any matches that don't have all three items (a siteurl, logid, and shareddriveid) into another. The source arrays will be different sizes. My current code looks like this:
const combined = folderMap.concat(mapping);
const applicable = combined.filter(i => i[0]['siteurl'] && i[0]);
const byTeamUrl = combined.reduce((acc, item) => {
acc[item['siteurl']] = Object.assign(acc[item['siteurl']] || {}, item);
if (item.length > 2) {
Logger.log(item);
}
return acc;
}, {});
Nothing is output, but I know that there should be at least three items that are in both source arrays. What's going wrong here?

Google Apps Script doesn't currently support ECMAScript 2015 (ES6). Hence your arrow functions will not work.
Per Google's docs: Basic JavaScript Features
See this answer: https://stackoverflow.com/a/51628532/8245557
However, you can develop for Apps Script in TypeScript using clasp: Develop Apps Script using TypeScript

I was pushing an array to my object when I should have just been pushing fields. I also reformatted my code so the same object was passed from function 1 to function 2 where function 2 used findIndex to update the value.

Related

Prevent pushing to array if duplicate values are present

I'm mapping an array and based on data i'm pushing Option elements into an array as follows
let make_children: any | null | undefined = [];
buyerActivityResult && buyerActivityResult.simulcastMyAccount.data.map((item: { make: {} | null | undefined; }, key: any) => {
make_children.push(
<Option key={key}>{item.make}</Option>
);
});
Following data array has several objects and these objects have an attribute called model.
buyerActivityResult.simulcastMyAccount.data
I want to prevent pusing Options to my array if the attribute model has duplicate data. It only has to push once for all similar model values.
How can i do it?
I tried something like this
buyerActivityResult && buyerActivityResult.simulcastMyAccount.data.map((item: { model: {} | null | undefined; }, key: any) => {
model_children.indexOf(item.model) === -1 && model_children.push(
<Option key={key}>{item.model}</Option>
);
});
But still duplicate values are being pushed into my array.
Its difficult to tell what you are trying to achieve but it looks like a map may not be the right tool for the job.
A map returns the same sized length array as that of the original array that you are calling map on.
If my assumptions are correct, your buyerActivityResult.simulcastMyAccount.data array has duplicate values, and you want to remove these duplicates based on the model property? One way to achieve this would be to use the lodash library for this, using the uniq function:
const uniqueResults = _.uniq(buyerActivityResult.simulcastMyAccount.data, (item) => item.model);
The Array.prototype.map() method is supposed to be used for manipulating the data contained into the array performing the operation. To manipulate data from other variables I recommend to use a for-loop block.
If item.model is an object, the function Array.prototype.indexOf() always returns -1 because it compares the memory address of the objects and does not do a deep comparison of all properties values.
The usual solution to remove duplicate data from an array is converting the Array into a Set then back to an Array. Unfortunately, this works only on primary type values (string, number, boolean, etc...) and not on objects.
Starting here, I will review your source code and do some changes and explain why I would apply those changes. First of all, assuming the make_children array does not receive new attribution later in your code, I would turn it into a constant. Because of the initialization, I think the declaration is overtyped.
const make_children: any[] = [];
Then I think you try to do too much things at the same time. It makes reading of the source code difficult for your colleagues, for you too (maybe not today but what about in few weeks...) and it make testing, debugging and improvements nearly impossible. Let's break it down in at least 2 steps. First one is transforming the data. For example remove duplicate. And the second one create the Option element base on the result of the previous operation.
const data: { make: any }[] = buyerActivityResult?.simulcastMyAccount?.data || [];
let options = data.map((item) => !!item.model); // removing items without model.
// Here the hard part, removing duplicates.
// - if the models inside your items have a property with unique value (like an ID) you can implement a function to do so yourself. Take a look at: https://stackoverflow.com/questions/2218999/remove-duplicates-from-an-array-of-objects-in-javascript
// - or you can use Lodash library like suggested Rezaa91 in its answer
options = _.uniq(data, (item) => item.model);
Now you only have to create the Option elements.
for (var i = 0; i < options.length; i++) {
model_children.push(<Option key={i}>{options[i].model}</Option>);
}
// OR using the Array.prototype.map method (in this case, do not declare `model_children` at the beginning)
const model_children:[] = options.map((opt:any, i:number) => <Option key={i}>{opt.model}</Option>);
Despite the lack of context of the execution of the code you provided I hope my answer will help you to find a solution and encourage you to write clearer source code (for the sake of your colleagues and your future self).
PS: I do not know anything about ReactJs. forgive me my syntax mistakes.

appendRow returns [Ljava.lang.Object;# instead of values

I want to copy form submissions over to a different sheet so that the copied data can be edited without affecting the original submissions.
I have the following code:
function copy2(){
var responses = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("from");
var tracker = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("to");
var lastrow = responses.getLastRow();
var col = responses.getLastColumn();
var row = responses.getRange(lastrow, 1, 1, col).getValues();
tracker.appendRow([null,row[0]]);
Using null in appendRow helps you move the info over to the next column. However, it doesn't quite work with the row[0] array. If I remove the null it works fine, but I want the info copied on a column different that the first one.
Why Ljava.lang.Object?
Because you are using the older Rhino runtime that was written in Java. Hence when something unexpected happens you get a glimpse of the infrastructure GAS is built upon. Now, the java.lang.object is a base class in Java from which other objects, including arrays, are derived.
Since the appendRow method signature's only parameter accepts a one-dimensional array of values, your row[0], which contains an array (see what getvalues method returns), made it to the sheet as a string tag indicating that this was an object at runtime.
What to do in Rhino?
All solutions depend on taking [ null ] as your base array and using concat to append the rest of the first row, something like this: [ null ].concat(row[0]). You can also use push with a simple for loop for better performance.
What to do in V80?
As the other answer mentioned, your best bet is the spread syntax. You can also do a push(...row[0]) to avoid concatenation of arrays (since you immediately use and discard the copy resulting from [ null, ...row[0] ]).
0 See official docs on how to migrate to V8 to take advantage of new language features and improved speed.
Explanation:
The approach of using null is clearly a workaround and not a futureproof
solution. Namely, if you want to start pasting from column 4 you
would have to do [null,null,null,...row[0]] which is not the proper
way to do it in my opinion.
I would advice you to get rid of appendRow and null since you
want to paste the data from the second column onwards. Therefore,
use setValues() instead.
Replace:
tracker.appendRow([null,row[0]]);
with:
tracker.getRange(tracker.getLastRow()+1,2,1,row[0].length).setValues(row);
Complete Solution:
function copy2(){
var responses = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("from");
var tracker = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("to");
var lastrow = responses.getLastRow();
var col = responses.getLastColumn();
var row = responses.getRange(lastrow, 1, 1, col).getValues();
tracker.getRange(tracker.getLastRow()+1,2,1,row[0].length).setValues(row);
}
The row variable contains an array so you should use the spread operator with appendRow
Replace:
tracker.appendRow([null,row[0]]);
with:
tracker.appendRow([null,...row[0]]);
Make sure your project is enabled for Chrome V8 runtime.

Use a common function to handle change of the multiple array-based similar states on the user click in REACT

I have created a transfer list in react which have two box for transferring names between each other. (Left list to Right list ). So i have written a function handleAllRight which transfers all the element in the left list to 2nd list which basically are array and make the left list empty serving the purpose of handleAllRight button.
Now I introduced one more leftList2 and rightList2 which also have a handleAllRight functionality.
But i am not getting to write a common handleAllRight function for both of the list. The only thing in handleAllRight function which is different is the number of the list.
This is my handleAllRight function in which i have used if loop. But how to dynamically create it if any list is passed in it. It should take care of the transfer checking the id on which the user clicked.
const handleAllRight = listNumber => {
if (listNumber === 1) {
setRightlist1(leftlist1);
setleftlist1([]);
}
if(listNumber === 2){
setRightlist2(leftlist2)
setleftlist2([]);
}
};
But i do not want to use the condition as there could be many list so it would increase the size of the code lines.
The other thing i tried is to use the eval function. Its working for any number of transfer list But i do not want to use the eval too as it is not good practice to use.
For example i used eval like this
const handleAllRight = listNumber => {
eval(`setRightJobSeq${listNumber}(leftJobSeq${listNumber})`)
eval(`setleftJobSeq${listNumber}([])`);
}
Here is the codeSandbox link. https://codesandbox.io/s/trusting-water-ccymg?file=/src/App.js
Using Eval is not a good idea. I think you can do this. You can pass leftList, and functions setleftlist1 or setleftlist2, setRightList1 or setRightList2 as the parameters and access the dynamic functions with this
const handleAllRight = (leftList, setleftlist, setRightList) => {
this[setRightList](leftList);
this[setleftlist]([]);
};
So basically you have an array of array of collections. This structure seems gpod for your code:
const myData = [
{
list1: [...],
list2: [...],
},
{
list1: [...],
list2: [...] ,
},
]
Then you can push into this collection, remove items, and access them by their index

What's the best way to filter an array of objects to only show those objects which were added since the last time it was filtered?

My first function scrapes my employers site for a list of users who have completed a task and outputs a json file containing the results. The json file is organized as follows:
{"Completed":[{"task":"TitleOfTaskAnd01/01/2019", "name":"UsersFullName"},{"task":"TitleOfTaskAnd01/01/2019", "name":"UsersFullName"}...]}
My second function uses the aforementioned json file to automatically generate receipts. On calling these two functions again I would like to leave out all of the previously utilized data, and only generate receipts for the tasks that were not in the results of any previous calls, therefore avoiding the generation of duplicates.
I tried to filter the first array by the elements of the second array, however as far as I can tell you cannot compare objects, or even arrays for that matter. Here is the function I tried to adjust to my needs:
let myArray = myArray.filter( ( el ) => !toRemove.includes( el ) );
I expect that my use case is not too uncommon and there is already a body of experience regarding best practices in this situation. I prefer solutions that use just javascript, so that I can understand how to navigate the situation better in the future. If however you have a library/module solution that is welcomed as well. Thanks in advance.
The problem is that two objects are never equal (except they are references to the same object). To check for structural equality, you have to manually compare their properties:
myArray.filter(el => !toRemove.some(el2 => el.task === el2.task && el.name === el2.name));
While that works, it will be quite slow for a lot of elements as you compare each object of myArray against all objects of toRemove. To improve that, you could generate a unique hash out of the properties and add that hash into a Set:
const hash = obj => JSON.stringify([obj.name, obj.task]);
const remove = new Set(toRemove.map(hash));
const result = myArray.filter(el => !remove.has(hash(el)));
This will be O(n + m), whereas the previous solutions was O(n * m).

Can I make a "Virtual Array" in JavaScript?

I'm calling a JavaScript function that wants an array of things to display. It displays a count, and displays the items one by one. Everything works when I pass it a normal JavaScript array.
But I have too many items to hold in memory at once. What I'd like to do, is pass it an object with the same interface as an array, and have my method(s) be called when the function tries to access the data. And in fact, if I pass the following:
var featureArray = {length: count, 0: func(0)};
then the count is displayed, and the first item is correctly displayed. But I don't want to assign all the entries, or I'll run out of memory. And the function currently crashes when the user tries to display the second item. I want to know when item 1 is accessed, and return func(1) for item 1, and func(2) for item 2, etc. (i.e., delaying the creation of the item until it is requested).
Is this possible in JavaScript?
If I understand correctly, this would help:
var object = {length: count, data: function (whatever) {
// create your item
}};
Then, instead of doing array[1], array[2], et cetera, you'd do object.data(1), object.data(2), and so on.
Since there seems to be a constraint that the data must be accessed using array indexing via normal array indexing arr[index] and that can't be changed, then the answer is that NO, you can't override array indexing in Javascript to change how it works and make some sort of virtual array that only fetches data upon demand. It was proposed for ECMAScript 4 and rejected as a feature.
See these two other posts for other discussion/confirmation:
How would you overload the [] operator in Javascript
In javascript, can I override the brackets to access characters in a string?
The usual way to solve this problem would be to switch to using a method such as .get(n) to request the data and then the implementor of .get() can virtualize however much they want.
P.S. Others indicate that you could use a Proxy object for this in Firefox (not supported in other browsers as far as I know), but I'm not personally familiar with Proxy objects as it's use seems rather limited to code that only targets Firefox right now.
Yes, generating items on the go is possible. You will want to have a look at Lazy.js, a library for producing lazily computed/loaded sequences.
However, you will need to change your function that accepts this sequence, it will need to be consumed differently than a plain array.
If you really need to fake an array interface, you'd use Proxies. Unfortunately, it is only a harmony draft and currently only supported in Firefox' Javascript 1.8.5.
Assuming that the array is only accessed in an iteration, i.e. starting with index 0, you might be able to do some crazy things with getters:
var featureArray = (function(func) {
var arr = {length: 0};
function makeGetter(i) {
arr.length = i+1;
Object.defineProperty(arr, i, {
get: function() {
var val = func(i);
Object.defineProperty(arr, i, {value:val});
makeGetter(i+1);
return val;
},
configurable: true,
enumerable: true
});
}
makeGetter(0);
return arr;
}(func));
However, I'd recommend to avoid that and rather switch the library that is expecting the array. This solution is very errorprone if anything else is done with the "array" but accessing its indices in order.
Thank you to everyone who has commented and answered my original question - it seems that this is not (currently) supported by JavaScript.
I was able to get around this limitation, and still do what I wanted. It uses an aspect of the program that I did not mention in my original question (I was trying to simplify the question), so it is understandable that other's couldn't recommend this. That is, it doesn't technically answer my original question, but I'm sharing it in case others find it useful.
It turns out that one member of the object in each array element is a callback function. That is (using the terminology from my original question), func(n) is returning an object, which contains a function in one member, which is called by the method being passed the data. Since this callback function knows the index it is associated with (at least, when being created by func(n)), it can add the next item in the array (or at least ensure that it is already there) when it is called. A more complicated solution might go a few ahead, and/or behind, and/or could cleanup items not near the current index to free memory. This all assumes that the items will be accessed consecutively (which is the case in my program).
E.g.,
1) Create a variable that will stay in scope (e.g., a global variable).
2) Call the function with an object like I gave as an example in my original question:
var featureArray = {length: count, 0: func(0)};
3) func() can be something like:
function func(r) {
return {
f : function() {featureArray[r + 1] = func(r + 1); DoOtherStuff(r); }
}
}
Assuming that f() is the member with the function that will be called by the external function.

Categories

Resources