Converting cell coordinates without using a column number to column letter method? - google-apps-script

I'm trying to figure out what my options are here when I need to use a column number in a formula, and if I really need to write a column number to column letter method to accomplish what I'm trying to do.
See this method I have here:
createFormulas(lookupField, lookupColumns) {
// Iterate through the lookupColumn array
lookupColumns.forEach(value => {
let columnNumber = this.getColumn(this.headers, value);
let range = this.sheet.getRange(2, columnNumber, this.lastRow - 1, 1);
// range.setFormula('=$A2');
range.setFormula('=' + columnNumber + '2' ); // doesn't work obviously
})
}
I'm trying to add formulas in a column based on the column.
this.getColumn() returns the column number based on the column name being passed in.
let range sets the range I want to set the formula in
range.setFormula('=$A2') pastes this formula into range and updates the reference accordingly (i.e., $A3, $A4, etc.). This isn't the formula I ultimately want to use, just a simplified example.
I need to set the column in the reference dynamically, however.
What I have obviously won't work: range.setFormula('=' + columnNumber + '2' );. That would just result in something like 72 where 7 is the column number.
I know I can write a method that will convert the column number into a letter. I'm just surprised there isn't a built in method for doing that or some other native way of accomplishing this.
For example, in Excel VBA I think you can do something like "=" & Cells(2, columnNumber).Address or something like that (been a while, I could be wrong), which should equate to =A2, =A3, =A4, etc. in the range.
So before writing this column number to letter method, I just wanted to check: is that the only way to accomplish what I'm after or is there a native way of handling this that I'm just not seeing?

Actually, was able to do this using .getA1Notation().
Refactored to the following and it works as expected:
createFormulas(lookupField, lookupColumns) {
// Iterate through the lookupColumn array
lookupColumns.forEach(value => {
let columnNumber = this.getColumn(this.headers, value);
let formulaRange = this.sheet.getRange(2, columnNumber, this.lastRow - 1, 1);
let referenceRange = this.sheet.getRange(2, this.idColumn, this.lastRow - 1, 1);
formulaRange.setFormula("=" + referenceRange.getCell(1, 1).getA1Notation());
})
}

Column To Letters
I followed Yuri's path to the numbers to letter functions and I'm a bit baffled that we have forgotten that there are 26 letters in the alphabet and so after looking at the various functions at that reference none of them seem to have worked for me. So here's my replacement:
function colToletters(num) {
let a = " ABCDEFGHIJKLMNOPQRSTUVWXYZ";
if (num < 27) return a[num % a.length];
if (num > 26) {
num--;
let letters = '';
while (num >= 0) {
letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[num % 26] + letters;
num = Math.floor(num / 26) - 1;
}
return letters;
}
}
This will calculate the column letters for 1 to 1000 and I check all the way to 703 where the letters go to AAA and they look good all the way.

Just in case. Based on https://stackoverflow.com/a/64456745/14265469
function numberToLetters(num) {
// num--; // if you need 1 --> A, 2 --> B, 26 --> Z
let letters = '';
while (num >= 0) {
letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[num % 26] + letters;
num = (num - num % 26) / 26 - 1;
}
return letters;
}
console.log(numberToLetters(0)); // --> A
console.log(numberToLetters(25)); // --> Z
console.log(numberToLetters(26)); // --> AA

Related

Adding a range of numbers ignoring leading character

I have a list of numbers, some of them have leading underscores, some of them don't.
A
B
_12
34
99
_42
Which is the best way of adding up these numbers?
Note: I tried this custom script formula which for some reason doesn"t work (only returns the first item passed in the range), and anyway I guess there should be an easier way just using native GoogleSheet formulas.
function sum_with_underscores(underscored_nums) {
let nums = underscored_nums.map( x => String(x).replace("_", ""))
return nums.reduce((pv, cv) => parseFloat(pv) + parseFloat(cv), 0);
}
=SUM(ARRAYFORMULA(VALUE(SUBSTITUTE(<your_range>, "_", ""))))
Assuming you would like to achieve this result using a script instead of a formula, the code below worked for me.
Make sure to flatten the array before manipulating the data. Hope this helps.
let values = sheet.getDataRange().getValues() // returns array of arrays
let nums = values.flat() // converts [[_12, 34.0], [99.0, _42]] to [_12, 34.0, 99.0, _42]
nums = nums.map( n => {
return typeof(n) === 'number' ? n : parseFloat(n.replace("_", ""));
})
let sum = nums.reduce((acc, num)=> acc + num, 0)
}

Is there a single Apps Script function equivalent to MATCH() with TRUE?

I need to write some functions that involve the same function as the Sheets function MATCH() with parameter 'sort type' set to TRUE or 1, so that a search for 35 in [10,20,30,40] will yield 2, the index of 30, the next lowest value to 35.
I know I can do this by looping over the array to search, and testing each value against my search value until a value greater than the search value is found, but it seems to me there must be a shorthand way of doing this. We don't have to do this when seeking an exact value; we can just use indexOf(). I was surprised when I first learned that indexOf() does not have a parameter for search type, but can only return a -1 if an exact value is not found.
Is there no function akin to indexOf() that will do this, or is it actually necessary to loop over the array every time you need to do this?
Probably you're looking for the array.find() method. The impelentation could be something like this:
var arr = [10,20,30,40]
// make a copy of the array, reverse it and do find with condition
var value = arr.slice().reverse().find(x => x < 35)
console.log(value) // output --> 30 (first element less than 35 in the reversed array)
var index = arr.indexOf(value)
console.log(index) // output --> 2 (index of the element in the original array)
https://www.w3schools.com/jsref/jsref_find.asp
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
There is another method array.findIndex(). Probably you can use it as well:
var arr = [10,20,30,40]
// find more or equal 35 and return previous index
var index = arr.findIndex(x => x >= 35) - 1
console.log(index) // output --> 2
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex
Try this:
function lfunko(tgt = 35) {
Logger.log([10,20,30,40].reduce((a,c,i) => { a.r = (a.x >= c)? i:a.r;return a;},{x:tgt}).r)
}

Convert DDMMYY to DD-MM-YY in SSIS Derived Column

I am facing issue while placing "-" in a varchar datatype.
. Need is after last two digits we need to put "-" and then again after two digits and so on.
Input String is- 21220 Output String- 2-12-20
Or,
Input String- 311220 Output String- 31-12-20
Can anyone help me on this ?
I would go with a script component for string manipulation because it's much easier to do and it's more readable.
1) Add a script component as a transformation
2) Under input columns, check your date column (I called this myDate in the example below)
3) Under Inputs and Outputs, under Output 0, add a column for the new date, called formattedDate below
4) In the script, modify the Input0_ProcessInputRow method. Before parsing the string, make sure it's not null. Then pad it with a "0" on the left to insure that we always have a length of 6. Parse, the date elements and set the value for the new column.
public override void Input0_ProcessInputRow(Input0Buffer Row)
{
if (!Row.mydate_IsNull)
{
var paddedDate = Row.mydate.PadLeft(6, '0');
var day = paddedDate.Substring(0, 2);
var month = paddedDate.Substring(2, 2);
var year = paddedDate.Substring(4, 2);
Row.formattedDate = $"{day}-{month}-{year}";
}
}
(DT_WSTR, 2)(DT_I4)LEFT(RIGHT("0" + #[User::date], 6), 2)
+ "-" + SUBSTRING(RIGHT("0" + #[User::date], 6), 3, 2)
+ "-" + RIGHT(#[User::date] , 2)

Partial replace in docs what matches only and preserve formatting

Let's assume that we have first paragraph in our google document:
Wo1rd word so2me word he3re last.
We need to search and replace some parts of text but it must be highlighted in editions history just like we changed only that parts and we must not loose our format (bold, italic, color etc).
What i have/understood for that moment: capturing groups didn't work in replaceText() as described in documentation. We can use pure js replace(), but it can be used only for strings. Our google document is array of objects, not strings. So i did a lot of tries and stopped at that code, attached in this message later.
Can't beat: how i can replace only part of what i've found. Capturing groups is very powerful and suitable instrument, but i can't use it for replacement. They didn't work or i can replace whole paragraph, that is unacceptable because of editions history will show full paragraph replace and paragraphs will lose formatting. What if what we searching will be in each and every paragraph, but only one letter must be changed? We will see full document replacement in history and it will be hard to find what really changed.
My first idea was to compare strings, that replace() gives to me with contents of paragraph then compare symbol after symbol and replace what is different, but i understand, that it will work only if we are sure that only one letter changed. But what if replace will delete/add some words, how it can be synced? It will be a lot bigger problem.
All topics that i've found and read triple times didn't helped and didn't moved me from the dead point.
So, is there any ideas how to beat that problem?
function RegExp_test() {
var docParagraphs = DocumentApp.getActiveDocument().getBody().getParagraphs();
var i = 0, text0, text1, test1, re, rt, count;
// equivalent of .asText() ???
text0 = docParagraphs[i].editAsText(); // obj
// equivalent of .editAsText().getText(), .asText().getText()
text1 = docParagraphs[i].getText(); // str
if (text1 !== '') {
re = new RegExp(/(?:([Ww]o)\d(rd))|(?:([Ss]o)\d(me))|(?:([Hh]e)\d(re))/g); // v1
// re = new RegExp(/(?:([Ww]o)\d(rd))/); // v2
count = (text1.match(re) || []).length; // re v1: 7, re v2: 3
if (count) {
test1 = text1.match(re); // v1: ["Wo1rd", "Wo", "rd", , , , , ]
// for (var j = 0; j < count; j++) {
// test1 = text1.match(re)[j];
// }
text0.replaceText("(?:([Ww]o)\\d(rd))", '\1-A-\2'); // GAS func
// #1: \1, \2 etc - didn't work: " -A- word so2me word he3re last."
test1 = text0.getText();
// js func, text2 OK: "Wo1rd word so-B-me word he3re last.", just in memory now
text1 = text1.replace(/(?:([Ss]o)\d(me))/, '$1-B-$2'); // working with str, not obj
// rt OK: "Wo1rd word so-B-me word he-C-re last."
rt = text1.replace(/(?:([Hh]e)\d(re))/, '$1-C-$2');
// #2: we used capturing groups ok, but replaced whole line and lost all formatting
text0.replaceText(".*", rt);
test1 = text0.getText();
}
}
Logger.log('Test finished')
}
Found a solution. It's a primitive enough but it can be a base for a more complex procedure that can fix all occurrences of capture groups, detect them, mix them etc. If someone wants to improve that - you are welcome!
function replaceTextCG(text0, re, to) {
var res, pos_f, pos_l;
var matches = text0.getText().match(re);
var count = (matches || []).length;
to = to.replace(/(\$\d+)/g, ',$1,').replace(/^,/, '').replace(/,$/, '').split(",");
for (var i = 0; i < count; i++) {
res = re.exec(text0.getText())
for (var j = 1; j < res.length - 1; j++) {
pos_f = res.index + res[j].length;
pos_l = re.lastIndex - res[j + 1].length - 1;
text0.deleteText(pos_f, pos_l);
text0.insertText(pos_f, to[1]);
}
}
return count;
}
function RegExp_test() {
var docParagraphs = DocumentApp.getActiveDocument().getBody().getParagraphs();
var i = 0, text0, count;
// equivalent of .asText() ???
text0 = docParagraphs[i].editAsText(); // obj
if (text0.getText() !== '') {
count = replaceTextCG(text0, /(?:([Ww]o)\d(rd))/g, '$1A$2');
count = replaceTextCG(text0, /(?:([Ss]o)\d(me))/g, '$1B$2');
count = replaceTextCG(text0, /(?:([Hh]e)\d(re))/g, '$1C$2');
}
Logger.log('Test finished')
}

ActionScript3 - add thousands separator to negative values

This question relates to an animated map template which we have developed at the UKs Office for National Statistics. It has been applied to many datasets and geographies many uses without problem. For example,
http://www.ons.gov.uk/ons/interactive/vp3-census-map/index.html
http://www.statistica.md/pageview.php?l=ro&idc=390&id=3807
The .fla calls on a supporting .as file (see below) to introduce a thousand separator (in the UK a comma, in Germany a full stop (period) defined elsewhwere.
However, the dataset I am currently mapping has large negative values, and it tutrns out that the ORIGINAL HELPER FUNCTION below does not like negative values with 3, 6, 9 or 12 (etc) digits.
-100 to -999 for instance are rendered NaN,100 to NaN,999.
This is because such values are recognised as being 4 digits long. They are being split, the comma introduced, and the -ve sign is misunderstood.
I reckon the approach must be to use absolute values, add in the comma and then (for the negative values) add the -ve sign back in afterwards. But so far, trials of the ADAPTED HELPER FUNCTION have produced only error. :-(
Can anyone tell me how to put the -ve sign back in , please?
Many thanks.
Bruce Mitchell
==================================================================================
//ORIGINAL HELPER FUNCTION: ACCEPTS A NUMBER AND RETURNS A STRING WITH THOUSANDS SEPARATOR ATTACHED IF NECESSARY
function addThouSep(num) {
/*
a. Acquire the number - 'myTrendValue' or 'myDataValue' - from function calcValues
b. Record it (still as a number) to data precision.
1. Turn dataORtrend into a string
2. See if there is a decimal in it.
3. If there isn't, just run the normal addThouSep.
4. If there is, run addThouSep just on the first bit of the string - then add the decimal back on again at the end.
*/
var myNum:Number = correctFPE(num); // Create number variable myNum and populate it with 'num'
// (myTrendvalue or myData Value from calcValues function) passed thru 'correctPFE'
var strNum:String = myNum+""; // Create string version of the dataORtrend number - so instead of 63, you get '63'
var myArray = strNum.split("."); // Create array representing elements of strNum, split by decimal point.
//trace(myArray.length); // How long is the array?
if (myArray.length==1) { // Integer, no decimal.
if (strNum.length < 4)//999 doesn't need a comma.
return strNum;
return addThouSep(strNum.slice(0, -3))+xmlData.thouSep+strNum.slice(-3);
}
else { // Float, with decimal
if (myArray[0].length < 4)//999 doesn't need a comma
return strNum;
return (addThouSep(myArray[0].slice(0, -3))+xmlData.thouSep+myArray[0].slice(-3)+"."+myArray[1]);
}
}
==================================================================================
//ADAPTED HELPER FUNCTION: ACCEPTS A NUMBER AND RETURNS A STRING WITH THOUSANDS SEPARATOR ATTACHED IF NECESSARY
function addThouSep(num) {
/*
a. Acquire the number - 'myTrendValue' or 'myDataValue' - from function calcValues
b. Record it (still as a number) to data precision.
1. Turn dataORtrend into a string
2. See if there is a decimal in it.
3. If there isn't, just run the normal addThouSep.
4. If there is, run addThouSep just on the first bit of the string - then add the decimal back on again at the end.
*/
var myNum:Number = correctFPE(num); // Create number variable myNum and populate it with 'num'
// (myTrendvalue or myData Value from calcValues function) passed thru 'correctPFE'
var myAbsNum:Number = Math.abs(myNum); // ABSOLUTE value of myNum
var strNum:String = myAbsNum+""; // Create string version of the dataORtrend number - so instead of 63, you get '63'
var myArray = strNum.split("."); // Create array representing elements of strNum, split by decimal point.
//trace(myArray.length); // How long is the array?
if (myNum <0){ // negatives
if (myArray.length==1) { // Integer, no decimal.
if (strNum.length < 4)//999 doesn't need a comma.
return strNum;
return addThouSep(strNum.slice(0, -3))+xmlData.thouSep+strNum.slice(-3);
}
else { // Float, with decimal
if (myArray[0].length < 4)//999 doesn't need a comma
return strNum;
return (addThouSep(myArray[0].slice(0, -3))+xmlData.thouSep+myArray[0].slice(-3)+"."+myArray[1]);
}
}
else // positive
if (myArray.length==1) { // Integer, no decimal.
if (strNum.length < 4)//999 doesn't need a comma.
return strNum;
return addThouSep(strNum.slice(0, -3))+xmlData.thouSep+strNum.slice(-3);
}
else { // Float, with decimal
if (myArray[0].length < 4)//999 doesn't need a comma
return strNum;
return (addThouSep(myArray[0].slice(0, -3))+xmlData.thouSep+myArray[0].slice(-3)+"."+myArray[1]);
}
}
==================================================================================
If you're adding commas often (or need to support numbers with decimals) then you may want a highly optimized utility function and go with straightforward string manipulation:
public static function commaify( input:Number ):String
{
var split:Array = input.toString().split( '.' ),
front:String = split[0],
back:String = ( split.length > 1 ) ? "." + split[1] : null,
pos:int = input < 0 ? 2 : 1,
commas:int = Math.floor( (front.length - pos) / 3 ),
i:int = 1;
for ( ; i <= commas; i++ )
{
pos = front.length - (3 * i + i - 1);
front = front.slice( 0, pos ) + "," + front.slice( pos );
}
if ( back )
return front + back;
else
return front;
}
While less elegant it's stable and performant — you can find a comparison suite at my answer of a similar question https://stackoverflow.com/a/13410560/934195
Why not use something simple like this function I've made?
function numberFormat(input:Number):String
{
var base:String = input.toString();
base = base.split("").reverse().join("");
base = base.replace(/\d{3}(?=\d)/g, "$&,");
return base.split("").reverse().join("");
}
Tests:
trace( numberFormat(-100) ); // -100
trace( numberFormat(5000) ); // 5,000
trace( numberFormat(-85600) ); // -85,600
Explanation:
Convert the input number to a string.
Reverse it.
Use .replace() to find all occurrences of three numbers followed by another number. We use $&, as the replacement, which basically means take all of those occurences and replace it with the value we found, plus a comma.
Reverse the string again and return it.
Did you try using the built in Number formatting options that support localized number values:
Localized Formatting with NumberFormatter