Google Active Script: using map in an array with 2 arguments - arrays

This works:
Suppose I want to use in an ArrayFormula() the indirect() function, which doesn't work. That is, supposed I type this in cell E1, =ArrayFormula(indirect(address(row(E:E),column(A:A)))), this will return the value of cell A1 in all the cells in column E. To circumvent this, I created the custom function below:
function retValue(cell){
if(cell.map) {
return cell.map(retValue);
} else {
var cellRang = SpreadsheetApp.getActive().getRange(cell);
return cellRang.getValue();
}
}
Now, when I enter this =ArrayFormula(retValue(address(row(E:E),column(A:A)))) in cell E1, each cell in column E will have the corresponding value of the cell in the same row of column A.
My problem:
What I need is to have a custom function that receives 2 arguments, like function retValue2(cell, anotherRange) but I only care if cell is an array, as anotherRange must be an array anyway. What happens is that when I iteratively call cell.map(retValue2) the argument anotherRange is lost and I'm not entirely sure how to go about this.
I tried to come up with this:
function retValue2(cell, anotherRange) {
if (cell.map) {
return cell.map(retValue2);
} else {
var range = SpreadsheetApp.getActive().getRange(anotherRange);
var nrRows = range.getNumRows();
var nrCols = range.getNumColumns();
return cell + ',' + nrRows + ',' + nrCols;
}
}
But it fails because anotherRange is not recognized inside the iteration I think. How do I solve this?
PS.: in the example that works, why exactly does it work at all? I understand that when I do this return cell.map(retValue); it will use my own function as the callback, which would return all the values in the array, but in the spreadsheet it shows only the one on the same row. What is the magic here?
EDIT:
My end goal is to create my own lookup function where I pass a search key and a 2-dimensional array (rows and columns) and then, it locates the coordinates of that key in the array.
Look here:
function retCoord(sKey, sIRange) {
try {
var key = SpreadsheetApp.getActive().getRange(sKey).getValue();
}
catch(e) {
var key = sKey;
}
var range = SpreadsheetApp.getActive().getRange(sIRange).getValues();
nbRow = range.length;
nbColumn = range[0].length;
for(var i = 0; i<nbRow; i++){
for(var j = 0; j<nbColumn; j++){
if(range[i][j] == key){
return i + ", " + j;
}
}
}
}
If in my spreadsheet I enter something like =retCoord("K4","A:L") it will search the content of cell K4 in my 2-dimensional array A:L and return where in the array the value is, 1, 2 for example. It also works if I use =retCoord(K4,"A:L") or =retCoord("term searched","A:L"), and in this latter case I enter directly the term searched. This works fine until I use it in an ArrayFormula().
First, instead of =retCoord("K4","A:L") I could very well use =retCoord(address(4, 11),"A:L") for instance and my .getRange() method would get the cell K4 just fine.
Now, here is the big problem. I want to use my function in an ArrayFormula(), and, positioning my cursor in O1 and hoping to search the items from column D in the columns E through L I want to pass as one of the inputs of address(), row(O:O), like this: =ArrayFormula(retCoord(address(row(O:O),4,4),"E:L")), meaning that for each row, a new address is passed. that is, in O1 cell, it should return the result of retCoord(D1,"E:L"), in O2 should be retCoord(D2,"E:L"), in O3 should be retCoord(D3,"E:L") and so on.
The issue happens because in my function, sKey is an array and if I try to use the same approach as my function retValue (here above in the This works: section) it fails because now, in retCoord, I have 2 inputs, and the introspection function calling from before fails because of the second input. Of course I'm missing something and there is always a better and more elegant way to approach a problem. But for now, can anyone help me with this one?
EDIT2:
I changed the code a little and it seems I moved forward but not quite yet. Check comment below for line indicated by (*):
function retCoord(sKey, sIRange) {
var key = '';
try {
key = SpreadsheetApp.getActive().getRange(sKey).getValue();
return key;
}
catch(e) {
if (sKey.map) {
var objKey = sKey.map(retCoord);
return objKey; // (*) <--- comments below
key = objKey;
} else {
key = sKey;
}
}
var range = SpreadsheetApp.getActive().getRange(sIRange).getValues();
nbRow = range.length;
nbColumn = range[0].length;
for(var i = 0; i<nbRow; i++){
for(var j = 0; j<nbColumn; j++){
if(range[i][j] == key){
return key + " = "+ i + ", " + j;
}
}
}
}
This (*) line I added only to see what was returning from the map. Surprisingly (in a way), it is an object with all the elements of that column and that is expected. What I didn't expect was that if I return that object to my spreadsheet cell, it brings back only the value of that specific cell (as I wanted but not really as expected). But the problem is I cannot use that object to compare against a string as it will expand and become something else and will never match. Look:
If I do return objKey in my custom function, over cell O1 it returns "a", in O2 it returns
"b", in O3 it returns "c" as expected because those are the values
of my cells D1, D2, D3 respectively.
If I do return "-> " + objKey
in in my custom function, instead of returning -> a, -> b, -> c in O1, O2, O3 respectively, it returns -> =A:A,a,b,c,d,e,f for all the cells in column O, which seems it did some type of objKey.toString() under the hood before concatenating with "-> "
Conclusion: how do I "coerce" the apparent result of objKey into string keeping the apparent result when you return the object without changing it? Simply put, I want the concatenation "some string" + to be equal to "some string" + "one string representing the value in that row instead of an object". In other words, what the hell is happening here? How does Google Sheets now that in that row, that element is the one representing the one I want? This is what I asked in the "PS." in the first part of this post.

You want to search all keys in the sKey column within the Range sIRange and note the position of each key within the range into the corresponding row in a destination column ?
This is how you can do it with Apps Script without formulas:
function retCoord(sKeyColumn, destinationColumn, sIRange) {
var key = sKeyColumn;
var range=SpreadsheetApp.getActive().getRange(sIRange);
var rangeValues = SpreadsheetApp.getActive().getRange(sIRange).getValues();
var nbRow = rangeValues.length;
var nbColumn = rangeValues[0].length;
var sKeyRange=SpreadsheetApp.getActive().getRange(sKeyColumn);
var destinationRange=SpreadsheetApp.getActive().getRange(destinationColumn);
var sKeyValues=sKeyRange.getValues();
for(var k=0;k<sKeyValues.length;k++){
for(var i = 1; i<=nbRow; i++){
for(var j = 1; j<=nbColumn; j++){
if(range.getCell(i, j).getValue() == sKeyValues[k][0]){
destinationRange.getCell(k+1, 1).setValue(range.getCell(i, j).getA1Notation());
}
}
}
}
}
Sample call:
function myFunction(){
retCoord('A1:A6','B1:B6','C1:J7');
}
retCoord('A:A','B:B','C:J'); would also work but would take very long, since the code would also loop through empty rows

Related

I am trying to create multiple loops but if one loop returns null it doesn't run the rest of the loop. How do I get around this?

I have multiple for loops in one code, as I am trying to collect data based off identifier "Y". There are some sheets that do not have identifier "Y" and those sheets always cause my script to stop running as the error cannot find the length in that specific for loop since there is none.
How do I get around this? I've tried if/else in everywhere I think would work and it's not working.
I've tried if/else statements and breaks but I must not be putting them in the right spot.
var VDSLr = VDSL.getRange('A:AS');
var VDSLraw = VDSLr.getValues();
var VDSLdata = []
for (var i = 0; i< VDSLraw.length ; i++){
if (VDSLraw[i][44] == "Y")
{
VDSLdata.push(VDSLraw[i])
}
Pull.getRange(Pull.getLastRow()+1,1, VDSLdata.length,
VDSLdata[0].length).setValues(VDSLdata);
}
var ITr = IT.getRange('A:AS');
var ITrawdata = ITr.getValues();
var ITd= []
for (var i = 0; i< ITrawdata.length ; i++){
if(ITrawdata[i][44] == "Y")
{
ITd.push(ITrawdata[i])
}
Pull.getRange(Pull.getLastRow()+1,1, ITd.length,
ITd[0].length).setValues(ITd);
}
**Edit: it won't let me post a picture of my error yet. Here's a few
examples though:
Error (1):**
var VDSLr = VDSL.getRange('A:AS');
var VDSLraw = VDSLr.getValues();
var VDSLdata = []
for (var i = 0; i< VDSLraw.length ; i++){
if (VDSLraw[i][44] != "Y")**continue**;
if (VDSLraw[i][44] == "Y");
{
VDSLdata.push(VDSLraw[i])
}
}
Pull.getRange(Pull.getLastRow()+1,1, VDSLdata.length,
VDSLdata[0].length).setValues(VDSLdata);
var ITr = IT.getRange('A:AS');
var ITrawdata = ITr.getValues();
var ITd= []
for (var i = 0; i< ITrawdata.length ; i++){
if (ITrawdata[i][44] != "Y")continue;
if (ITrawdata[i][44] == "Y")
{
ITd.push(ITrawdata[i])
}
}
Pull.getRange(Pull.getLastRow()+1,1, ITd.length,
ITd[0].length).setValues(ITd);
So if I put a continue here, it won't read the length. Error code is "Cannot read property "length" from undefined. Which I know is correct. This is what I'm trying to bypass. "VDSL" is a sheet that normally does not have what I am looking for. But since it won't find the length it won't continue to sheet "IT."
I have also moved around the continues to different spots, and even a break (in the same spot as continue) but my combinations seem to provide a never ending loop of the same info.
The other attempt is with if else statements. When I use those I get a synthax error. On this error(2) I've tried:
if (VDSLraw[i][44] == "Y")
{//code here}
else if (VDSLraw[i][44] != "Y"){break};
I feel like I'm making this more complicated than it is, should be a simple if/then statement but since I have to pull the data and compile it to one sheet the for loop is the best way to go. Just can't figure out the last piece. I could do them separately but that's my last resort. There's 12 sheets, so clicking 12 scripts each time I need to do this doesn't seem efficient.
as soon as you provided limited information about your data I made some assumptions, I hope it matches your needs.
function getY_Data(){
// get the current Spreadsheet;
var currentSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
// get all tabs on the file, so you wont need to keep track of how many tabs you have
var tabsArray = currentSpreadsheet.getSheets();
var resultsSheet = currentSpreadsheet.getSheetByName("results");
var currentSheetValues;
var collected_Y_Rows = [];
// iterate over all tabs
tabsArray.forEach(function(tabObject,index){
// skip the results tab
if(tabObject.getName() === "results"){ continue; }
// set the current sheet data to currentSheetValues variable
currentSheetValues = tabsArray[0].getDataRange().getValues();
// Im considering that all your sheets has the same row size
currentSheetValues.forEach(function(row,index){
if(row[44] == "Y"){
collected_Y_Rows.push(row);
}
});
});
// if it hasnt find any row with "Y" it won't clear the "results" tab or try do add empty data to the "results" sheet
if(collected_Y_Rows.length === 0){
return;
}
// clear the current values and formats
resultsSheet.clear();
// append new data starting on row 1 and col 2;
// Im considering that all your sheets has the same row size;
// if the sheets tabs has different sizes of data (columns) it will throw an error because when using the range.setValues(collected_Y_Rows),
// all rows on "collected_Y_Rows" mas have the same size;
resultsSheet.getRange(1, 1, collected_Y_Rows.length, collected_Y_Rows[0].length).setValues(collected_Y_Rows);
}

Get two arrays, compare and remove duplicates in Google Apps Script

I have got two lists on a google spreadsheet: 'Existing Companies' and 'New Companies'.
I would like to compare the two and find out which unique entries in 'New Companies' do not exist in 'Existing Companies', get those entries and eliminate from 'New Companies' all other entries.
I have made the following script to do it:
function grabNewCompanies() {
// grab existing companies list from sheet into an array
var sh = SpreadsheetApp.openById("sheetID").getSheetByName("sheetName")
var row = sh.getDataRange().getLastRow()
var existingCompanies = sh.getRange(2,1,row - 1,1).getValues()
Logger.log(existingCompanies)
//grab new companies added
var sh = SpreadsheetApp.openById("sheetID").getSheetByName("sheetName")
var row = sh.getDataRange().getLastRow()
var newCompanies = sh.getRange(2,4,row - 1, 1).getValues()
Logger.log(newCompanies)
var array = [];
for(i=0; i<newCompanies.length; i++) {
for(j=0; j<existingCompanies.length; j++) {
if(newCompanies[i][0] !== existingCompanies[j][0]) {
array.push([newCompanies[i][0]]);
}
Logger.log(array)
}
I have ran this script but it has failed.
The two arrays (existingCompanies and newCompanies) are returned correctly.
However, the comparison between the two does not seem to be working: it always returns the first element of the newCompanies array, regardless of whether it exists in existingCompanies.
Also, I am unsure about how to ensure that the values pushed into array are not duplicated if newCompanies contains more than one entry which does not exist in existingCompanies.
Thank you.
You want to retrieve the difference elements between existingCompanies and newCompanies. If my understand for your question is correct, how about this modification? I think that there are several solutions for your situation. So please think of this as one of them.
Modification points:
In the case that your script is modified, it picks up an element from newCompanies and it checks whether that is included in existingCompanies.
If that picked element is not included in existingCompanies, the element is pushed to array. In this modification, I used true and false for checking this.
This flow is repeated until all elements in newCompanies are checked.
Modified script 1:
When your script is modified, how about this?
From:
var array = [];
for(i=0; i<newCompanies.length; i++) {
for(j=0; j<existingCompanies.length; j++) {
if(newCompanies[i][0] !== existingCompanies[j][0]) {
array.push([newCompanies[i][0]]);
}
}
}
To:
var array = [];
for(i=0; i<newCompanies.length; i++) {
var temp = false; // Added
for(j=0; j<existingCompanies.length; j++) {
if(newCompanies[i][0] === existingCompanies[j][0]) { // Modified
temp = true; // Added
break; // Added
}
}
if (!temp) array.push([newCompanies[i][0]]); // Modified
}
Logger.log(array)
Modified script 2:
As other patterns, how about the following 2 samples? The process cost of these scripts are lower than that of the script using for loop.
var array = newCompanies.filter(function(e) {return existingCompanies.filter(function(f) {return f[0] == e[0]}).length == 0});
Logger.log(array)
or
var array = newCompanies.filter(function(e) {return !existingCompanies.some(function(f) {return f[0] == e[0]})});
Logger.log(array)
If I misunderstand your question, please tell me. I would like to modify it.

How to find all sequences of three in an array of values

first question ever here...
I am coding a simple 3-card poker hand evaluator and am having problems finding/extracting multiple "straights" (sequential series of values) from an array of values.
I need to extract and return EVERY straight the array possibly has. Here's an example:
(assume array is first sorted numerically incrementing)
myArray = [1h,2h,3c,3h,4c]
Possible three-value sequences are:
[1h,2h,3c]
[1h,2h,3h]
[2h,3c,4c]
[2h,3h,4c]
Here is my original code to find sequences of 3, where the array contains card objects with .value and .suit. For simplicity in this question I just put "2h" etc here:
private var _pokerHand = [1h,2h,3c,3h,4c];
private function getAllStraights(): Array
{
var foundStraights:Array = new Array();
for (var i: int = 0; i < (_handLength - 2); i++)
{
if ((_pokerHand[i].value - _pokerHand[i + 1].value) == 1 && (_pokerHand[i + 1].value - _pokerHand[i + 2].value) == 1)
{
trace("found a straight!");
foundStraights.push(new Array(_pokerHand[i], _pokerHand[i + 1], _pokerHand[i + 2]));
}
}
return foundStraights;
}
but it of course fails when there are value duplicates (like the 3's above). I cannot discard duplicates because they could be of different suits. I need every possible straight as in the example above. This allows me to run the straights through a "Flush" function to find "straight flush".
What array iteration technique am I missing?
This is an interesting problem. Given the popularity of poker games (and Flash) I'm sure this has been solved many times before, but I couldn't find an example online. Here's how I would approach it:
Look at it like a path finding problem.
Begin with every card in the hand as the start of a possible path (straight).
While there are possible straights:
Remove one from the list.
Find all the next valid steps, (could be none, or up to 4 following cards with the same value), and for each next valid step:
If it reaches the goal (completes a straight) add it to a list of found straights.
Otherwise add the possible straight with the next step back to the stack.
This seems to do what you want (Card object has .value as int):
private function getAllStraights(cards:Vector.<Card>, straightLength:uint = 3):Vector.<Vector.<Card>> {
var foundStraights:Vector.<Vector.<Card>> = new <Vector.<Card>>[];
var possibleStraights:Vector.<Vector.<Card>> = new <Vector.<Card>>[];
for each (var startingCard:Card in cards) {
possibleStraights.push(new <Card>[startingCard]);
}
while (possibleStraights.length) {
var possibleStraight:Vector.<Card> = possibleStraights.shift();
var lastCard:Card = possibleStraight[possibleStraight.length - 1];
var possibleNextCards:Vector.<Card> = new <Card>[];
for (var i:int = cards.indexOf(lastCard) + 1; i < cards.length; i++) {
var nextCard:Card = cards[i];
if (nextCard.value == lastCard.value)
continue;
if (nextCard.value == lastCard.value + 1)
possibleNextCards.push(nextCard);
else
break;
}
for each (var possibleNextCard:Card in possibleNextCards) {
var possibleNextStraight:Vector.<Card> = possibleStraight.slice().concat(new <Card>[possibleNextCard]);
if (possibleNextStraight.length == straightLength)
foundStraights.push(possibleNextStraight);
else
possibleStraights.push(possibleNextStraight);
}
}
return foundStraights;
}
Given [1♥,2♥,3♣,3♥,4♣] you get: [1♥,2♥,3♣], [1♥,2♥,3♥], [2♥,3♣,4♣], [2♥,3♥,4♣]
It gets really interesting when you have a lot of duplicates, like [1♥,1♣,1♦,1♠,2♥,2♣,3♦,3♠,4♣,4♦,4♥]. This gives you:
[1♥,2♥,3♦], [1♥,2♥,3♠], [1♥,2♣,3♦], [1♥,2♣,3♠], [1♣,2♥,3♦], [1♣,2♥,3♠], [1♣,2♣,3♦], [1♣,2♣,3♠], [1♦,2♥,3♦], [1♦,2♥,3♠], [1♦,2♣,3♦], [1♦,2♣,3♠], [1♠,2♥,3♦], [1♠,2♥,3♠], [1♠,2♣,3♦], [1♠,2♣,3♠], [2♥,3♦,4♣], [2♥,3♦,4♦], [2♥,3♦,4♥], [2♥,3♠,4♣], [2♥,3♠,4♦], [2♥,3♠,4♥], [2♣,3♦,4♣], [2♣,3♦,4♦], [2♣,3♦,4♥], [2♣,3♠,4♣], [2♣,3♠,4♦], [2♣,3♠,4♥]
I haven't checked this thoroughly but it looks right at a glance.

Checking arrays in AS3

I'm collecting rows of answers from a database which are made in to arrays. Something like:
for (var i:int = 0; i < event.result.length; i++) {
var data = event.result[i];
var answer:Array = new Array(data["question_id"], data["focus_id"], data["attempts"], data["category"], data["answer"], data["correct"], data["score"]);
trace("answer: " + answer);
restoreAnswer(answer, i);
}
Now, if I trace answer, I typically get something like:
answer: 5,0,2,IK,1.a,3.1,0
answer: 5,0,1,IK,2.a,3.1,0
answer: 4,1,1,AK,3,3,2
From this we see that focus_id 0 (second array item) in question_id 5 (first array item) has been attempted twice (third array item), and I only want to use the last attempt in my restoreAnswer function.
My problem is that first attempt answers override the second attempts since the first are parsed last it seems. How do I go about only calling my restoreAnswer only when appropriate?
The options are:
1 attempts, correct score (2 points)
2 attempts, correct score (1 points)
1 attempt, wrong score (0 points)
2 attemps, wrong score (0 points)
There can be multiple focus_id in each question_id.
Thank you very much! :)
I would consider having the DB query return only the most recent attempt, or if that doesn't work efficiently, return the data in attempt order. You may score question 5 twice, but at least it'll score correctly on the last pass.
You can also filter or sort the data you get back from the server.
Michael Brewer-Davis suggested using the database query to resolve this; normally speaking, this would be the right solution.
If you wait until you get it back from the web method call or whatever in AS3, then consider creating an additional Vector variable:
var vAttempts:Vector.<Vector.<int>> = new Vector.<Vector.<int>>(this.m_iNumQuestions);
You mentioned that it seems that everything is sorted so that earlier attempts come last. First you want to make sure that's true. If so, then before you do any call to restoreAnswer(), you'll want to check vAttempts to make sure that you have not already called restoreAnswer() for that question_id and focus_id:
if (!vAttempts[data["question_id"]])
{
vAttempts[data["question_id"]] = new Vector.<int>(); // ensuring a second dimension
}
if (vAttempts[data["question_id"]].indexOf(data["focus_id"]) == -1)
{
restoreAnswer(answer, i);
vAttempts[data["question_id"]].push(data["focus_id"]);
}
So optimizing this just a little bit, what you'll have is as follows:
private final function resultHandler(event:ResultEvent):void {
var vAttempts:Vector.<Vector.<int>> = new Vector.<Vector.<int>>(this.m_iNumQuestions);
var result:Object = event.result;
var iLength:int = result.length;
for (var i:int = 0; i < iLength; i++) {
var data = result[i];
var iQuestionID:int = data["question_id"];
var iFocusID:int = data["focus_id"];
var answer:Array = [iQuestionID, iFocusID, data["attempts"],
data["category"], data["answer"], data["correct"], data["score"]];
trace("answer: " + answer);
var vFocusIDs:Vector.<int> = vAttempts[iQuestionID];
if (!vFocusIDs) {
vAttempts[iQuestionID] = new <int>[iFocusID];
restoreAnswer(answer, i);
} else if (vFocusIDs.indexOf(iFocusID) == -1) {
restoreAnswer(answer, i);
vFocusIDs.push(iFocusID);
}
}
}
Note: In AS3, Arrays can skip over certain indexes, but Vectors can't. So if your program doesn't already have some sort of foreknowledge as to the number of questions, you'll need to change vAttempts from a Vector to an Array. Also account for whether question IDs are 0-indexed (as this question assumes) or 1-indexed.

How would I remove a "row" in an array depending on the value of an element?

Here's what I'm currently doing/trying to do to accomplish my goal. But it is not removing the "row" the way I would like it too.
So, I'm making an object, then pushing it into an array. And the adding to the array part works fine and just as I expect.
var nearProfileInfoObj:Object = new Object();
nearProfileInfoObj.type = "userInfo";
nearProfileInfoObj.dowhat = "add";
nearProfileInfoObj.userid = netConnection.nearID;
nearProfileInfoObj.username = username_input_txt.text;
nearProfileInfoObj.sex = sex_input_txt.selectedItem.toString();
nearProfileInfoObj.age = age_input_txt.selectedItem;
nearProfileInfoObj.location = location_input_txt.text;
nearProfileInfoObj.headline = headline_input_txt.text;
theArray.push(nearProfileInfoObj);
So after that later on I need to be able to remove that object from the array, and it's not working the way I'm expecting. I want to take a variable whoLeft and capture their ID and then look in the array for that particular ID in the userid part of the object and if its there DELETE that whole "row".
I know you can do a filter with an array collection but that doesnt actually delete it. I need to delete it because I may be adding the same value again later on.
whoLeft = theiruserIDVariable;
theArray.filter(userLeaving);
public function userLeaving(element:*, index:int, arr:Array):Boolean
{
if (element.userid == whoLeft)
{
return false;
}
else
{
return true;
}
}
But this doesnt seem to be deleting the whole row like it implies. Does anyone know what i'm doing wrong?
Instead of modifying the original array, the new filtered array is returned by the filter method. So you need to assign the returned array to theArray.
Try this
theArray = theArray.filter(userLeaving);
EDIT This turned out to be slower than for loop:
An alternative to the hand coded loop could be something like this:
theArray.every(searchAndDestroy);
public function searchAndDestroy(element:*, index:int, arr:Array):Boolean
{
if (element.userid == whoLeft)
{
arr.splice(index,1);
return false;
}
return true;
}
As far as I know, every() terminates the first time the test function returns false. So the question is: for a big list, which is faster, the for loop or the loop that every() does with the overhead of the test function call.
EDIT #2 But this was faster than a for loop for a test I ran on an array of a million Points:
for each(var element:Object in theArray)
{
if (element.userid==whoLeft)
{
theArray.splice(theArray.indexOf(element),1);
break;
}
}
I think this is what you're looking for:
for(var i:uint = 0, len:uint = theArray.length; i<len; i++)
{
if(thisArray[i].id == whoLeft.id)
{
thisArray.splice(i, 1);
break;
}
}
However, do you really need it in an Array because you could always use a Dictionary which would mean accessing it by id which would be a lot simpler to remove.

Resources