Sheets Custom Function - Arrays? or Ranges? What is the difference? - javascript

I am trying to write the most basic of functions and struggling The purpose of the script is to take 2 inputs (ranges) and then use those cells to construct a single, long string of the contents of all the cells concatenated....it doesn't work (see code below).
It has something to do with a range not really being an array because when I hardcoded the array it worked fine. If I have this layout:
Header1 Header2
Comment1 Comment2
Comment1a Comment2a
Note that each string is in its own cell in the spreadsheet. What I want is to get:
Header1
Comment1
Comment1a
Header2
Comment2
Comment2a
BUT all of this should be in a single cell.
Instead, what happens is:
Header1, Header2
Comment1, Comment1a, Comment2, Comment2a
all in a single cell.
Here is my current code....totally confused and no documentation anywhere intelligible doesn't help.
function concatComments(headerRange,commentRange)
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var out=''
newline = String.fromCharCode(10)
hVals = headerRange.split()
cVals = commentRange.split()
return hVals
// Test whether input is an array.
if (headerRange.map && commentRange.map)
{
/* process array or range */
// headerRange = ['Header1','Header2']
// commentRange = ['comment1','comment2', 'comment2a']
// var hValues = ss.getRange(headerRange).getValues()
// var cValues = ss.getRange(commentRange).getValues()
for( i=0; i< commentRange.length; i++)
{
if(commentRange[i].length >= 0)
{
out += headerRange[i] + newline + " " + commentRange[i] + newline
}
}
}
else
{
/*process single value */
out = headerRange[0] + newline + " " + commentRange[0]
}
return out
}
If anyone could explain what is going on I would greatly appreciate it.
Thanks in advance.
paul

You can also do this without Apps Script with the JOIN Sheets function, e.g.
=JOIN(" ", A1:A3)
Will concatenate the values in A1:A3 separated by a space.
For your example, you'd need something more complicated like
=JOIN(CHAR(10),A1, ARRAYFORMULA(" " & A2:A3), B1, ARRAYFORMULA(" " & B2:B3))
[Example spreadsheet]

Extracting Arrays from Google Sheets with Google Apps Scripts using the:
getRange().getValues() methods
Header1 Header2 .getValues() for headers
Comment1 Comment2 | .getValues() for comments
Comment1a Comment2a | ^^^^^^^^^^^^^^^^^^^^^^
Use the following test arrays as this is what will be returned by getValues():
[["Header1","Header2"]]
[["Comment1", "Comment2"],["Comment1a", "Comment2a"]]
so, being that array indices are zero based:
headers[0][1] == "Header2"
comments[1][0] == "Comment1a"
comments[1] == ["Comment1a", "Comment2a"]
Empty cells are represented by ""
Comment1 Comment2
Comment2a
Would be represented as:
[["Comment1", "Comment2"],["", "Comment2a"]]
each row is an array within the larger array and every row array will contain the same number of elements even when some cells are empty.
To find out the number of:
Rows comments.lengthreturns the number of row arrays
Columns comments[0].lengthreturns the number of elements(columns) in the first row array and being that all row arrays contain the same number of elements only one need be used to derive the number of columns.
See if this works:(arguments are returned as value arrays therefore .getValues() is not used)
function concatComments(headerRange,commentRange) {
var out=''
var newline = String.fromCharCode(10);
for(var c = 0; c < headerRange[0].length; c++) {
if( c > 0 ) out = out + newline;
out = out + headerRange[0][c] + newline;
for(var r = 0; r < commentRange.length; r++) {
if(commentRange[r][c] != "") {
out = out + " " + commentRange[r][c] + newline;
}
}
}
return out;
}

Related

Using JS to sum a column of values from an external text file containing donation histories of a db of donors

I need some assistance figuring out how to sum a column of dynamic totals that could be a positive or negative dollar amount, or an indication of stock shares.
I have a tab-delimited text file of donor contributions for that I am matching up against a CSV file of other related customer data that I am using to create a statement letter which will show a "donation history" of a particular donor. Each donor has a different amount of donations, and to complicate things, the column of data for a particular donation record could show either "$1,000.00" or "($1,000.00)" or "2 Shares APPL". The number with the parentheticals is of course, representing a negative number.
At the end of this column, I need to show a string that will read either "Total: $1,000.00," or if any of the donation history contains a donation record that included shares of stock the returned string will simply read, "$1,000.00 & Stock."
I have been racking my brain trying to come up with the JS rule that can achieve this. I have the JS rule that is generating the donation history correctly, but summing the donation amount column is causing me to go crazy...
Here is the JS for generating my donation history list in the letter (this seems to be working fine):
var contributionList = new ExternalDataFileEx("/~wip/248839 Frontiers/Master Data/Double Data proof.txt", "\t");
var donor_id = Field("Supporter");
var lb = "<br>\n";
var matches = new Array();
for (var i = 0; i <= contributionList.recordCount; i++) {
var idVariable = contributionList.GetFieldValue(i, "Supporter");
var dateVariable = contributionList.GetFieldValue(i, "Donation Date");
var ministryVariable = contributionList.GetFieldValue(i, "Ministry Designation");
var giftVariable = contributionList.GetFieldValue(i, "Donation Amount");
var tsSettings = "<p tabstops=19550,Right,,;29600,Left,,;>";
var ts = "<t>";
if (donor_id == idVariable)
matches.push(tsSettings + dateVariable + ts + giftVariable + ts + ministryVariable);
}
//return matches;
return matches.join(lb);
Now here is the JS code that is not working just fine. I am trying to tally the donation amount column, it only returns "Total: $0.00 & Stock" every time (I have tried to explain my thought process via comments):
var contributionList = new ExternalDataFileEx("/~wip/248839 Frontiers/Master Data/Double Data proof.txt", "\t");
var donor_id = Field("Supporter");
for (var i = 0; i <= contributionList.recordCount; i++) {
var idVariable = contributionList.GetFieldValue(i, "Supporter");
var giftVariable = contributionList.GetFieldValue(i, "Donation Amount");
var sum = 0;
var shares = 0;
var tsSettings = "<p tabstops=19550,Right,,;29600,Left,,;>";
var ts = "<t>";
var totalStr = "Total ";
var stockStr = " & Stock";
var totalFormatted = FormatNumber("$#,###.00", Math.max(0, StringToNumber(sum)));
// Match data from linked file to current Supporter
if (donor_id == idVariable) {
// Look at current record and see if it contains the word "Share(s)"
// or not and act accordingly
if (giftVariable.match(/(^|\W)share($|\W)/i) || giftVariable.match(/(^|\W)shares($|\W)/i)) {
// Turn switch "on" if donation amount is a share or shares so
// we can have the " & Stock" appended to our string.
shares = 1;
// Because this donation is/are shares, we must "zero" this
// amount to make the math work when we sum everything up...
giftVariable = 0;
// This is where we are keeping our running total...
sum += giftVariable[i];
} else {
// This record was not a donation of share(s) so we now have to
// determine whether we are dealing with postive or negative numbers
// and then strip out all of the non-number characters, remove and
// replace the () whis just a "-," leaving us with a number we can
// work with...
// If number has parenthesis, then deal with it...
if (giftVariable.indexOf("(")) {
// Strip out all the ()$, characters...
giftVariable = giftVariable.replace(/[()$,]/g,"")
// Append the minus sign to the number...
giftVariable = "-" + giftVariable;
sum += giftVariable[i];
} else {
giftVariable = giftVariable.replace(/[$,]/g,"");
sum += giftVariable[i];
}
}
}
}
// Return Total...
if (shares == 1) {
return tsSettings + totalStr + ts + totalFormatted + stockStr;
} else {
return tsSettings + totalStr + ts + totalFormatted;
}
Any assistance would be greatly appreciated!
The problem (and code) needs to be broken into smaller, atomic steps. From your description it sounds like you should:
load a text file into memory
for each line in the file
extract: {
donor_id
charity
gift
and store the results in a contributions dictionary
for each item in the contributions dictionary
transform gift string into {
dollarAmount: float with a default of 0.0
stock: name with a default of ""
}
create an empty dictionary called totals
each item will have the shape {
id
dollarAmount as a float
stocks an an array
}
for each item in the contributions dictionary
lookup the id in the totals dictionary
if it exists
totals[id].dolarAmount += item.dollarAmount
totals[id].stocks.push(item.stock)
otherwise
totals[id].dollarAmount = item.dollarAmount
totals[id].stocks = [item.stock]
normalize your charities
for each item in totals dictionary
remove any empty strings from item.charities
create your report
for each item in totals dictionary
write`${item.id} donated `${item.dollarAmont}` ${item.stocks.length > 1 ? 'and stock' : ''
I believe you are trying to do too many things at once. Instead, the goal should be to normalize your data before you attempt to perform any calculations or aggrgrations, then normalize your aggregrations before writing your summaries or reports.
I would also stay away from using any direct string manipulation. You should have a dedicated function whose only purpose is to take a string like "($20.34) and 1 share of APPL" and return either 20.34, -20.34, or 0.0. And a different function whose only purpose is to take the same string and return either true or false is stock was present.

Google Script Errror: "Incorrect range width" when using setValues

Try as I might I CANNOT decipher the problem that I'm having writing new rows to a sheet. I've done this several times and I've debugged this thoroughly using Logger.log, but I just can't solve it. Here's a summary of what I'm doing, a code snippet, and a log:
What I'm doing:
Adding rows to a sheet (below existing rows)
73 new rows are stored stored in array: Grade Rows
When attempt to write the new rows to the sheet, get this error:
Incorrect range width, was 1 should be 26
Here’s the code including some Logger.logs:
var BeginningRow = LastSGRowSheet + 1;
var EndingRow = BeginningRow + SGPushKtr -1;
Logger.log("BeginningRow =>" + BeginningRow + "<=, SGPushKtr =>" + SGPushKtr + "<=, Ending Row =>" + EndingRow + "<=");
var GradesRangeString = 'A' + BeginningRow + ':' + LastStudentGradesColumnLetter + EndingRow;
Logger.log("GradesRangeString =>" + GradesRangeString + "<=");
StudentGradeSheet.getRange(GradesRangeString).setValues(GradeRows);
The error occurs in that last line of code.
Here’s the log:
17-12-31 11:51:15:763 EST] BeginningRow =>364<=, SGPushKtr =>73<=, Ending Row =>436<=
[17-12-31 11:51:15:764 EST] GradesRangeString =>A364:Z436<=
Let's say that your data array is dA then the number of rows in that array is dA.length and assuming its a rectangular array then the number of columns is vA[0].length. So your output command has to be some thing like this.
sheet.getRange(firstRow,firstColumn,dA.length,dA[0].length).setValues(dA);
If you'd like to learn a little more about this problem check this out.
You could also append each row to the current sheet one row at a time in loop.
It's hard to know why GradeRows doesn't match your range without seeing all of your code.
Using Cooper's getRange arguments will likely reveal your problem, and will prevent you from having to update your row and column variables when you make changes to your code. Another issue that gets me sometimes is the fact that the setValues array needs to be exactly the same dimensions as the range. If one row has a different length, it will fail. If the logic I use to create row arrays can result in different lengths, I use the function below to make sure my arrays are symmetric before writing them to a sheet. It is also helpful for debugging.
/**
* Takes a 2D array with element arrays with differing lengths
* and adds empty string elements as necessary to return
* a 2D array with all element arrays of equal length.
* #param {array} ar
* #return {array}
*/
function symmetric2DArray(ar){
var maxLength;
var symetric = true;
if (!Array.isArray(ar)) return [['not an array']];
ar.forEach( function(row){
if (!Array.isArray(row)) return [['not a 2D array']];
if (maxLength && maxLength !== row.length) {
symetric = false;
maxLength = (maxLength > row.length) ? maxLength : row.length;
} else { maxLength = row.length }
});
if (!symetric) {
ar.map(function(row){
while (row.length < maxLength){
row.push('');
}
return row;
});
}
return ar
}
How about using appendRow()? That way you don't need to do lots of calculations about the range. You can loop through your data and add it row by row. Something like this:
myDataArr = [[1,2],[3,4],[5,6]]
myDataArr.forEach(function(arrayItem){
sheet.appendRow([arrayItem[0],arrayItem[1]])
})
// This will output to the sheet in three rows.
// [1][2]
// [3][4]
// [5][6]

Google Sheets conditional combine, clear all cells in range, Loop. VBA to JS

I'm translating VBA into a Google Sheets script, but am a loss on how to get past this loop through all rows in a single column hurdle.
I have a column which contains an #OrdernNumber, a -State code, a First name, and sometimes a Last name, followed by an empty cell. I need to combine the First and Last names into the First name cell, and then clear the Last name cell below. This is illustrated in the screenshot below:
Column C and D in the screenshot reveal the pattern I used to construct my original VBA code:
Sub WorkingCombineAndClearLoop()
Dim Rngcell As Range
For Each Rngcell In Range("B1:B100")
'if first character is #
If left(Rngcell.Value, 1) <> "#" _
'and if first character is -
And left(Rngcell.Value, 1) <> "-" _
'and if cell is not blank then
And Rngcell.Value <> "" Then
'combine left cell
Rngcell.Value = Rngcell.Offset(0, -1).Value _
'with bottom left cell
& " " & Rngcell.Offset(1, -1).Value
'then clear below cell
Rngcell.Offset(1, 0).ClearContents
End If
Next
End Sub
Following is my annotated JavaScript code. Because I can't get the loop working yet, I've had to change the code logic a bit:
function workingCombineAndClearNoLoop() {
//get active spreadsheet, sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
//determine row count
var numRows = SpreadsheetApp.getActiveSheet().getRange("A:A").getLastRow();
//if A1's first character is not # AND
if (s.getRange('A1').getValue().substring(0,1) === "#" &&
//if A2's first character is not ' AND
s.getRange('A1').offset(1, 0).getValue().substring(0,1) === "-" ) {
//Set the value of A2 to...
s.getRange('A1').offset(2, 1).setValue(
//A2 cell content + " " + A2 cell content (concatenate)
s.getRange('A1').offset(2, 1).getValue() + " " +
s.getRange('A1').offset(3, 1).getValue())
//and then clear A2
s.getRange('A1').offset(3, 1).clearContent()
};
}
The following is where everything falls apart - the loop. I've only left the bare bones in this final copy of the code:
function notWorkingCombineAndClearWithLoop() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var numRows = SpreadsheetApp.getActiveSheet().getRange("A:A").getLastRow();
//loop
//how to replace 'A1' with A[i]?, so that it loops through all cells in the column?
for (var i = 0; i < 25; i++) {
if (s.getRange('A1').getValue().substring(0,1) === "#" &&
s.getRange('A1').offset(1, 0).getValue().substring(0,1) === "-" ) {
s.getRange('A1').offset(2, 1).setValue(
s.getRange('A1').offset(2, 1).getValue() + " " +
s.getRange('A1').offset(3, 1).getValue())
s.getRange('A1').offset(3, 1).clearContent()
}
};
}
Note: I've completed Codacademy's JS course and several Google Sheets tutorials, and am pretty good at VBA, but I'm still swimming in circles with this. I've tried to follow many loop examples, using variables in place of A1. Because I can't find any working solution, I've left the non-functioning A1 as a simple placeholder.
Short answer
//how to replace 'A1' with A[i]?, so that it loops through all cells
//in the column?
Use
'A' + i
Explanation
getRange() has several forms, one of them use A1 notation reference. This is used in concordance with the Google Apps Script included in the question.
Example
The main intention of below code is to show how to use the A1 notation to loop through the cells of a column in Google Apps Script.
There was some errors in the indexes. They were corrected.
Also changed the fourth line to reduce the rows to loop through and added a couple of lines.
function editedNotWorkingCombineAndClearWithLoop() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var numRows = SpreadsheetApp.getActiveSheet().getLastRow();
/*
* use variables to reduce number of calls to Apps Script services
* and improve readability
*/
var range, substring1, substring2;
//loop
for (var i = 1; i <= numRows; i++) {
//Here is the magic
range = s.getRange('A' + i );
substring1 = range.getValue().substring(0,1);
substring2 = range.offset(1, 0).getValue().substring(0,1);
if ( substring1 === '#' && substring2 === '-' ) {
range.offset(0, 1).setValue(range.getValue());
range.offset(1, 1).setValue(range.offset(1, 0).getValue());
range.offset(2, 1).setValue(
range.offset(2, 0).getValue()
+ " "
+ range.offset(3, 0).getValue()
)
}
}
}

If statement check

I have an if statement that loops through csv data. There are 51 rows that I have parsed in by new lines and by commas to give me individual arrays for each row. My issue is my if statement doesn't seem to be working and honestly I just don't see why. Raw is the csv file.
Updated heres how i split the csv file
console.log("Loaded: " + data.length);
console.log("feature layer graphics: " + fl.graphics.length);
console.log(data);
var raw = data.split("\n");
var valMin = Infinity;
var valMax = -Infinity;
var arrayForBreaks = [];
console logs returns 51 false
then there is one console log of true
for (var i = 0; i < raw.length; i++) {
var row = raw[i].split(",");
var name = " ";
var amount = " ";
if (row.length >= 6) {
name = row[0];
amount = row[1];
}
console.log(isNaN(row[1]));
stateData[name] = row;
valMin = Math.min(valMin, row[1]);
valMax = Math.max(valMax, row[1]);
arrayForBreaks.push(parseFloat(amount));
}; //end for loop
So my question is in the if statement I'm checking to see if the length of the first element in the array is greater that 6 if it is dump that name and amount in those variables. I also had a else statement to fill in the last array. But it seems to keep adding one empty line to the end of the data.
So my question is in the if statement I'm checking to see if the length of the first element in the array is greater that
Negative.
You are checking to see if there are more than 6 elements in the array, or 6 comma delimited items.
To check the length of the first element you would need to:
if (row[0].length >= 6) {
....
[…] in the if statment I'm checking to see if the lengh of the first element in the array is greater tha[n] 6
No, that’s not what you are doing. row.length will return the number of elements of row – and row is an array you got from splitting the raw string by commas. If you want to check the length of the first element, you should check row[0].length instead.

JavaScript filtering an array of <input> values by character count

This should be a quickie, but I'm scratching my head as to why this bit of JavaScript isn't working for me. The goal is to take the value of an input box (string of words separated by spaces), list these words as items in an array, and remove those which are fewer than 3 characters:
var typed = $('input').val();
var query = typed.split(" ");
var i=0;
for (i=0; i<query.length; i++) {
if (query[i].length < 3) {
query.splice(i,1);
}
}
Have this running onkeyup for the input box and it seems to work, but only about 50% of the time (strings of 1 and 2 characters somehow find their way into the array on occasion). Any suggestions would be hugely appreciated.
The problem is that you are iterating while removing the elements. Consider this array:
["he", "l", "lo world"]
Initially your loop starts at index 0 and removes "he" from the array. Now the new array is
["l", "lo world"]
In the next iteration i will be 1, and you will check "lo world"'s length, thus ignoring the "l" string altogether.
Use the filter method in Array to remove the unwanted elements.
var biggerWords = query.filter(function(word) {
return word.length >= 3;
});
Besides the iterating problem, you may also see unexpected entries if you type multiple spaces
try
var query = typed.split(/\s+/);
This way it will split on any number of spaces, instead of each individual one
The problem is that you're slicing the array while counting forward. Think about it...if you take an index point out of the array, thereby shortening it by one, incrementing i and moving on to the next one actually moves one further than you want, completely missing the next index. Increment i--, start at query.length-1, and make the condition that i>=0. For an example of this in action, check it out here:
http://jsfiddle.net/kcwjs/
CSS:
input {
width:300px;
}​
HTML:
<input id="textbox" type="text" />
<div id="message"></div>​
Javascript:
$(document).ready(function() {
$('#textbox').keyup(checkStrings);
});
function checkStrings(e) {
var typed = $('#textbox').val();
if (typed == "") return false;
var query = typed.split(" ");
var querylen = query.length;
var acceptedWords = '';
var badWords = '';
for (var i = querylen-1; i >= 0; i--) {
if (query[i].length < 3) {
badWords += query[i] + " ";
} else {
acceptedWords += query.splice(i,1) + " ";
}
}
$('#message').html("<div>Bad words are: " + badWords + "</div>" +
"<div>Good words are: " + acceptedWords + "</div>");
}
Try this code, it get's rid of any 3 character words, as well as making sure no empty array elements are created.
typed.replace(/(\b)\w{1,3}\b/g,"$1");
var query = typed.split(/\s+/);
hey i think you should use a new array for the result. since you are removing the element in array. the length is changed. here is my solution
var typed = "dacda cdac cd k foorar";
var query = typed.split(" ");
var i=0;
var result = [];
for (i=0; i<query.length; i++) {
if (query[i].length >= 3) {
result.push(query[i]);
}
}

Categories