Writing scripts for Google Spreadsheets can be difficult sometimes, because the Google Spreadsheet methods that take row and column numbers use 1-based indexing, while Javascript arrays use 0-based.
In this example, cell A2 has a row == 2 and column == 1. The SpreadsheetApp methods reverse column & row from those in A1Notation, so these two ranges are equivalent:
var range1 = sheet.getRange("A2");
var range2 = sheet.getRange(2, 1);
Once I read the contents of a sheet into an array, things are different again.
var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
var data = sheet.getDataRange().getValues();
After that, the value that was in cell A2 in my spreadsheet is in data[1][0]. The row & column are in the same order as the SpreadsheetApp API, but each is 1 less.
The answers to lots of questions here (example) have boiled down to mismatching of these different forms of indexing. Code that is full of row+1 and col-1 statements can be hard to debug.
Finally: If I know the reference for a cell in A1Notation, say AZ342, how can I find out the correct index values correspond to that cell in the 2D array, data, obtained from the full Data Range?
Obviously, you can just be very careful about keeping track of the places where you're using either type of indexing, and you'll be fine.
But it would be easier to do something like this:
var importantCell = "AZ342";
var cellIndexConverted = cellA1ToIndex( importantCell );
var data[cellIndexConverted.row][cellIndexConverted.col] = "Some new value";
ConvertA1.gs
Here are three helper functions to simplify conversion from A1Notation.
These helper functions are also available as a gist.
/**
* Convert a cell reference from A1Notation to 0-based indices (for arrays)
* or 1-based indices (for Spreadsheet Service methods).
*
* #param {String} cellA1 Cell reference to be converted.
* #param {Number} index (optional, default 0) Indicate 0 or 1 indexing
*
* #return {object} {row,col}, both 0-based array indices.
*
* #throws Error if invalid parameter
*/
function cellA1ToIndex( cellA1, index ) {
// Ensure index is (default) 0 or 1, no other values accepted.
index = index || 0;
index = (index == 0) ? 0 : 1;
// Use regex match to find column & row references.
// Must start with letters, end with numbers.
// This regex still allows induhviduals to provide illegal strings like "AB.#%123"
var match = cellA1.match(/(^[A-Z]+)|([0-9]+$)/gm);
if (match.length != 2) throw new Error( "Invalid cell reference" );
var colA1 = match[0];
var rowA1 = match[1];
return { row: rowA1ToIndex( rowA1, index ),
col: colA1ToIndex( colA1, index ) };
}
/**
* Return a 0-based array index corresponding to a spreadsheet column
* label, as in A1 notation.
*
* #param {String} colA1 Column label to be converted.
*
* #return {Number} 0-based array index.
* #param {Number} index (optional, default 0) Indicate 0 or 1 indexing
*
* #throws Error if invalid parameter
*/
function colA1ToIndex( colA1, index ) {
if (typeof colA1 !== 'string' || colA1.length > 2)
throw new Error( "Expected column label." );
// Ensure index is (default) 0 or 1, no other values accepted.
index = index || 0;
index = (index == 0) ? 0 : 1;
var A = "A".charCodeAt(0);
var number = colA1.charCodeAt(colA1.length-1) - A;
if (colA1.length == 2) {
number += 26 * (colA1.charCodeAt(0) - A + 1);
}
return number + index;
}
/**
* Return a 0-based array index corresponding to a spreadsheet row
* number, as in A1 notation. Almost pointless, really, but maintains
* symmetry with colA1ToIndex().
*
* #param {Number} rowA1 Row number to be converted.
* #param {Number} index (optional, default 0) Indicate 0 or 1 indexing
*
* #return {Number} 0-based array index.
*/
function rowA1ToIndex( rowA1, index ) {
// Ensure index is (default) 0 or 1, no other values accepted.
index = index || 0;
index = (index == 0) ? 0 : 1;
return rowA1 - 1 + index;
}
I wanted to post another solution since I usually think about converting strings using a loop rather than checking its length.
function convertSheetNotation(a1_notation) {
const match = a1_notation.match(/(^[A-Z]+)|([0-9]+$)/gm);
if (match.length !== 2) {
throw new Error('The given value was invalid. Cannot convert Google Sheet A1 notation to indexes');
}
const column_notation = match[0];
const row_notation = match[1];
const column = convertColumnNotationToIndex(column_notation);
const row = convertRowNotationToIndex(row_notation);
return [row, column];
}
function convertColumnNotationToIndex(a1_column_notation) {
const A = 'A'.charCodeAt(0);
let output = 0;
for (let i = 0; i < a1_column_notation.length; i++) {
const next_char = a1_column_notation.charAt(i);
const column_shift = 26 * i;
output += column_shift + (next_char.charCodeAt(0) - A);
}
return output;
}
function convertRowNotationToIndex(a1_row_notation) {
const num = parseInt(a1_row_notation, 10);
if (Number.isNaN(num)) {
throw new Error('The given value was not a valid number. Cannot convert Google Sheet row notation to index');
}
return num - 1;
}
Thank you to #Mogsdad as this is a small modification to your answer, although I believe my solution is less efficient.
Related
I am trying to solve a leetcode [problem][1]. The question says to search in a 2d array. While my code passes for most of the test cases it fails for a particular test case.
/**
* #param {number[][]} matrix
* #param {number} target
* #return {boolean}
*/
/**
* #param {number[][]} matrix
* #param {number} target
* #return {boolean}
*/
var searchMatrix = function(matrix, target) {
let i = 0 ;
let j = matrix.length ;
while(i <= matrix.length - 1 && j >= 0){
if(matrix[i][j] == target){
return true
}
if(matrix[i][j] > target){
j--;
} else {
i++;
}
}
return false
};
searchMatrix([1,3],3)
Above solution gives false whereas the correct answer should be true. What's wrong here ? Cant find out !
[1]: https://leetcode.com/problems/search-a-2d-matrix/
This question has two variations one on Leetcode and another on Geeks for Geeks.
Your solution above would work for GFG platform but would fail on leetcode.
Why?
Variation in both question lies how elements are arranged. In leetcode question first element of every row will be greater than last element of the previous row, which is not in the case of GFG. On GFG you will have row and column wise sorted matrix.
Your solution would pass the GFG test cases but would fail on leetcode. Hence one of the optimized solutions that you can go with is to use its question property and imagine it as a one dimensional array.
function searchMatrix(matrix, target){
let numberOfRows = matrix.length
let numberOFColums = matrix[0].length
let upperBoundOfMatrix = numberOfRows * numberOFColums - 1;
let start = 0
while(start <= upperBoundOfMatrix){
let mid = Math.floor(start + (upperBoundOfMatrix - start)/2);
let row = Math.floor(mid/numberOFColums);
let column = Math.floor(mid % numberOFColums);
if(matrix[row][column] == target){
return true
}
if(matrix[row][column] > target){
upperBoundOfMatrix = mid - 1;
} else {
start = mid + 1;
}
}
return false
}
This question already has answers here:
How to get a number of random elements from an array?
(25 answers)
Closed 4 years ago.
I got 4 Math.random generators. Each picking 1 of the X objects from the array.
var randomItem1 = projects[Math.floor(Math.random()*projects.length)];
var randomItem2 = projects[Math.floor(Math.random()*projects.length)];
var randomItem3 = projects[Math.floor(Math.random()*projects.length)];
var randomItem4 = projects[Math.floor(Math.random()*projects.length)];
How can I write a function that prevents the Math.random to generate the same number as other Math.random generator.
My guess:
Creating a loop that loops through the var randomItem 1 till 4. If it finds 1 or more outputs to be the same, it will regenerate a new output for 1 or more the duplicated outputs.
Any other suggestions?
Edit: This is for a website.
Thanks for the interesting problem. I always used a library for this sort of thing so it was fun figuring it out.
var projects
var randomProjects
function getRandomProjects(projects, sampleSize) {
var projectsClone = projects.slice();
var randomItems = [];
while (sampleSize--) {
randomItems.push(
projectsClone.splice(Math.floor(Math.random()*projectsClone.length), 1)[0]
);
}
return randomItems;
}
projects = ['1st project', '2nd project', '3rd project', '4th project', '5th project', '6th project'];
randomProjects = getRandomProjects(projects, 4);
randomProjects.forEach(function(randomProject) {
console.log(randomProject);
});
The projectsClone.splice(...) deletes a random project from projectsClone and returns it as a single item in an array ([<project>]). Thus in the next iteration of the loop that value (<project>) can no longer be chosen.
However I would suggest using a library if you are using this in production code. For example losdash's _.sampleSize(projects, 4)
Update
Removed unneeded parts like Set, for loop, and added splice(). This function will mutate the original array due to splice(). Using splice() on an array of consecutive ordered numbers (even after shuffling) guarantees a set of unique numbers.
/**
* genRan(quantity, length)
*
* Need a given amount of unique random numbers.
*
* Given a number for how many random numbers are to be returned (#quantity) and a
* number that represents the range of consecutive numbers to extract the random
* numbers from (#length), this function will:
* - generate a full array of numbers with the length of #length
* - use shuffle() to shuffle the array to provide an array of randomly ordered numbers
* - splices the first #quantity numbers of the shuffled array
* - returns new array of unique random numbers
*
* #param {Number} quantity length of returned array.
* #param {Number} length length of an array of numbers.
*
* #return {Array} An array of unique random numbers.
*/
Demo
Details commented in Demo
var qty = 4
var len = 100;
function genRan(quantity, length) {
// Ensure that quantity is never greater than length
if (quantity > length) {
quantity = length;
}
// Create an array of consecutive numbers from 1 to N
var array = Array.from({
length: length
}, (value, key = 0) => key + 1);
// Run shuffle get the returned shuffled array
var shuffled = shuffle(array);
// return an array of N (quantity) random numbers
return shuffled.splice(0, quantity);
}
console.log(genRan(qty, len));
/* shuffle utility
|| Uses Fisher-Yates algorithm
*/
function shuffle(array) {
var i = 0;
var j = 0;
var temp = null;
for (i = array.length - 1; i > 0; i -= 1) {
j = Math.floor(Math.random() * (i + 1))
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
I am trying to calculate the consistency of an array of values which output a percentage. So for example an array of [1,1,1,1,1] would output 100% consistency, and array of [1,12,23,5,2] would output something like 25% consistency.
I have tried a simple percentage difference calculation but is only for two numbers whereas this needs to work for a dynamic array and this only outputs the difference. Any help would be appreciated so thanks in advance.
function getPercentage(n1, n2){
var newValue = n1 - n2;
return (newValue / n1) * 100;
}
getPercentage(1, 12)
Based on your comments, I refined the base value used in calculating the overall consistency between members of the array.
The new results calculated (which on visual inspection seems plausible, again it depends on the definition of consistency over all members but I think this is getting closer):
$ node jsscratch.js
Consistency for 1,1,1,1,1 is 100.000% fixed to 3 decimal places
Consistency for 1,12,23,5,2 is 33.360% fixed to 3 decimal places
Consistency for 1,12,23,15,12 is 50.160% fixed to 3 decimal places
Consistency for 1,2,10 is 83.778% fixed to 3 decimal places
/**
* Calculate the expected value
*/
function expectancy(arrayOfValues) {
let sumTotal = function(previousValue, currentValue) {
return previousValue + currentValue;
};
let u = arrayOfValues.reduce(sumTotal);
// Assume each member carries an equal weight in expected value
u = u / arrayOfValues.length;
return u;
}
/**
* Calculate consistency of the members in the vector
* #param {Array<number>} The vector of members to inspect for similarity
* #return {number} The percentage of members that are the same
*/
var similarity = function(arrayOfValues) {
let sumTotal = function(previousValue, currentValue) {
return previousValue + currentValue;
};
// Step 1: Calculate the mean value u
let u = expectancy(arrayOfValues); // Calculate the average
// Step 2: Calculate the standard deviation sig
let sig = [];
let N = 1/arrayOfValues.length;
for (let i = 0; i < arrayOfValues.length; i++) {
sig.push(N*(arrayOfValues[i] - u) * (arrayOfValues[i] - u));
}
// This only works in mutable type, such as found in JavaScript, else sum it up
sig = sig.reduce(sumTotal);
// Step 3: Offset from 100% to get the similarity
return 100 - sig;
}
answer = similarity(ar1);
console.log(`Consistency for ${ar1} is ${answer.toFixed(3)}% fixed to 3 decimal places`);
answer = similarity(ar2);
console.log(`Consistency for ${ar2} is ${answer.toFixed(3)}% fixed to 3 decimal places`);
answer = similarity(ar3);
console.log(`Consistency for ${ar3} is ${answer.toFixed(3)}% fixed to 3 decimal places`);
answer = similarity(ar4);
console.log(`Consistency for ${ar4} is ${answer.toFixed(3)}% fixed to 3 decimal places`);
Ok this is my first stab I'll take at it, tried to brake it up into smaller units of code to make it easier to work through. If I had enough time this could be further refined but then the readability goes south ;-)
Tried to avoid using TypeScript and to keep it as ECMAScript 5 friendly as possible. (Sorry about the messy formatting)
let ar100 = [1,1,1,1,1];
let ar25 = [1,12,23,5,2];
/**
* Calculate the lack of difference between two values (ie consistency)
* #param {number} valA
* #param {number} valB
* #returns {number} The consistency as a decimal
* where 1 is '100% consistent'
* and 0.5 is '50% consistent'
*/
var calcConsistency = function( valA, valB) {
let diff = Math.abs(valA - valB);
let consistency = (valA - diff) / valA;
// assumption: you can't be less than zero consistent
return consistency < 0 ? 0 : consistency;
}
/**
* A predicate used by the map operation in 'overallConsistency'
* #param {*} val
* #param {*} index
* #param {*} values
* #return {number} the average consistency of the current 'val' to the other 'values'
*/
var mapEachConsistency = function(val, index, values) {
let predicateGetOnlyOthers = function(currentValue, othersIndex) {
return index !== othersIndex;
};
let others = values.filter(predicateGetOnlyOthers);
const otherCount = others.length;
let totalConsistency = others.reduce(function(previousValue, currentValue) {
return previousValue + calcConsistency(val, currentValue);
});
return (totalConsistency / otherCount);
}
/**
* Calculate the overall average consistency of an array of numbers
* #param {Array<number>} arrayOfValues
* #return {number} The average consistency score as percentage
*/
var overallConsistency = function(arrayOfValues) {
let consists = arrayOfValues.map(mapEachConsistency);
let sumTotal = function(previous, current){ return previous + current; };
let avgAnswer = consists.reduce(sumTotal);
return (avgAnswer / consists.length) * 100;
};
// Here's using it to see the results
var answer = overallConsistency(ar100);
console.log(`Consistency: ${answer.toFixed(3)}% (fixed to 3 dicimal place)`);
answer = overallConsistency(ar25);
console.log(`Consistency: ${answer.toFixed(3)}% (fixed to 3 dicimal place)`);
I would like get a random number in a range excluding one number (e.g. from 1 to 1000 exclude 577). I searched for a solution, but never solved my issue.
I want something like:
Math.floor((Math.random() * 1000) + 1).exclude(577);
I would like to avoid for loops creating an array as much as possible, because the length is always different (sometimes 1 to 10000, sometimes 685 to 888555444, etc), and the process of generating it could take too much time.
I already tried:
Javascript - Generating Random numbers in a Range, excluding certain numbers
How can I generate a random number within a range but exclude some?
How could I achieve this?
The fastest way to obtain a random integer number in a certain range [a, b], excluding one value c, is to generate it between a and b-1, and then increment it by one if it's higher than or equal to c.
Here's a working function:
function randomExcluded(min, max, excluded) {
var n = Math.floor(Math.random() * (max-min) + min);
if (n >= excluded) n++;
return n;
}
This solution only has a complexity of O(1).
One possibility is not to add 1, and if that number comes out, you assign the last possible value.
For example:
var result = Math.floor((Math.random() * 100000));
if(result==577) result = 100000;
In this way, you will not need to re-launch the random method, but is repeated. And meets the objective of being a random.
As #ebyrob suggested, you can create a function that makes a mapping from a smaller set to the larger set with excluded values by adding 1 for each value that it is larger than or equal to:
// min - integer
// max - integer
// exclusions - array of integers
// - must contain unique integers between min & max
function RandomNumber(min, max, exclusions) {
// As #Fabian pointed out, sorting is necessary
// We use concat to avoid mutating the original array
// See: http://stackoverflow.com/questions/9592740/how-can-you-sort-an-array-without-mutating-the-original-array
var exclusionsSorted = exclusions.concat().sort(function(a, b) {
return a - b
});
var logicalMax = max - exclusionsSorted.length;
var randomNumber = Math.floor(Math.random() * (logicalMax - min + 1)) + min;
for(var i = 0; i < exclusionsSorted.length; i++) {
if (randomNumber >= exclusionsSorted[i]) {
randomNumber++;
}
}
return randomNumber;
}
Example Fiddle
Also, I think #JesusCuesta's answer provides a simpler mapping and is better.
Update: My original answer had many issues with it.
To expand on #Jesus Cuesta's answer:
function RandomNumber(min, max, exclusions) {
var hash = new Object();
for(var i = 0; i < exclusions.length; ++i ) { // TODO: run only once as setup
hash[exclusions[i]] = i + max - exclusions.length;
}
var randomNumber = Math.floor((Math.random() * (max - min - exclusions.length)) + min);
if (hash.hasOwnProperty(randomNumber)) {
randomNumber = hash[randomNumber];
}
return randomNumber;
}
Note: This only works if max - exclusions.length > maximum exclusion. So close.
You could just continue generating the number until you find it suits your needs:
function randomExcluded(start, end, excluded) {
var n = excluded
while (n == excluded)
n = Math.floor((Math.random() * (end-start+1) + start));
return n;
}
myRandom = randomExcluded(1, 10000, 577);
By the way this is not the best solution at all, look at my other answer for a better one!
Generate a random number and if it matches the excluded number then add another random number(-20 to 20)
var max = 99999, min = 1, exclude = 577;
var num = Math.floor(Math.random() * (max - min)) + min ;
while(num == exclude || num > max || num < min ) {
var rand = Math.random() > .5 ? -20 : 20 ;
num += Math.floor((Math.random() * (rand));
}
import random
def rng_generator():
a = random.randint(0, 100)
if a == 577:
rng_generator()
else:
print(a)
#main()
rng_generator()
Exclude the number from calculations:
function toggleRand() {
// demonstration code only.
// this algorithm does NOT produce random numbers.
// return `0` - `576` , `578` - `n`
return [Math.floor((Math.random() * 576) + 1)
,Math.floor(Math.random() * (100000 - 578) + 1)
]
// select "random" index
[Math.random() > .5 ? 0 : 1];
}
console.log(toggleRand());
Alternatively, use String.prototype.replace() with RegExp /^(577)$/ to match number that should be excluded from result; replace with another random number in range [0-99] utilizing new Date().getTime(), isNaN() and String.prototype.slice()
console.log(
+String(Math.floor(Math.random()*(578 - 575) + 575))
.replace(/^(577)$/,String(isNaN("$1")&&new Date().getTime()).slice(-2))
);
Could also use String.prototype.match() to filter results:
console.log(
+String(Math.floor(Math.random()*10))
.replace(/^(5)$/,String(isNaN("$1")&&new Date().getTime()).match(/[^5]/g).slice(-1)[0])
);
I am returning an IP and then comparing it from a range.
I am just looking at the US IP and if it falls in a range I am displaying a messge. I am getting the IP correctly but when I am trying to match it with the range I am getting a syntax error of unexpected range. How should I resolve this?
here is how my code looks like
$.getJSON("http://jsonip.com", function (data) {
var x = data.ip;
$('body').html(x);
var usRange = [3.0.0.0, 222.229.21.255];
if (x >= usRange[0] && x <= usRange[1]) {
alert('US');
} else alert('outside US');
});
Here is my fiddle
What the error means, you can't assign number with 4 decimal dots.
var usRange = [3.0.0.0, 222.229.21.255]; //Not possible
FYI: The IP address returned from the json is string, not a number.
So you approach won't work. Here check my approach,
1) Split the ip address based on . returned by $.getJSON
var x = data.ip.split('.'); // returns ["122", "164", "17", "211"]
2) Instead of using usRange array just do it with simple comparison operations.
3) You cannot compare strings with numbers, so convert those strings to numbers like
+x[0] //convert "122" to 122
Finally,
$.getJSON("http://jsonip.com", function (data) {
var x = data.ip.split('.');
if (+x[0] >= 3 && +x[0] <= 222) {
if (+x[1] >= 0 && +x[1] <= 229) {
if (+x[2] >= 0 && +x[2] <= 21) {
if (+x[3] >= 0 && +x[3] <= 255) {
alert("Within range");
}
}
}
} else {
alert("Is not within range");
}
});
JSFiddle
I decided to rewrite my answer here for the sake of a better method. The first method actually converts the IP addresses to integers using bit shifting and then compares the integer values. I've left the second answer as an option but I prefer the first.
Neither of these functions will validate your IP addresses!
Method 1: Bit Shifting
/**
* Checks if an IP address is within range of 2 other IP addresses
* #param {String} ip IP to validate
* #param {String} lowerBound The lower bound of the range
* #param {String} upperBound The upper bound of the range
* #return {Boolean} True or false
*/
function isWithinRange(ip, lowerBound, upperBound) {
// Put all IPs into one array for iterating and split all into their own
// array of segments
var ips = [ip.split('.'), lowerBound.split('.'), upperBound.split('.')];
// Convert all IPs to ints
for(var i = 0; i < ips.length; i++) {
// Typecast all segments of all ips to ints
for(var j = 0; j < ips[i].length; j++) {
ips[i][j] = parseInt(ips[i][j]);
}
// Bit shift each segment to make it easier to compare
ips[i] =
(ips[i][0] << 24) +
(ips[i][1] << 16) +
(ips[i][2] << 8) +
(ips[i][3]);
}
// Do comparisons
if(ips[0] >= ips[1] && ips[0] <= ips[2]) return true;
return false;
}
Method 2: Plain 'Ol Logic Mess
/**
* Checks if an IP address is within range of 2 other IP addresses
* #param {String} ip IP to validate
* #param {String} lowerBound The lower bound of the range
* #param {String} upperBound The upper bound of the range
* #return {Boolean} True or false
*/
function isWithinRange(ip, lowerBound, upperBound) {
// Save us some processing time if the IP equals either the lower bound or
// upper bound
if (ip === lowerBound || ip === upperBound) return true;
// Split IPs into arrays for iterations below. Use same variables since
// we won't need them as strings anymore and because someone will complain
// about wasting memory.
ip = ip.split('.');
lowerBound = lowerBound.split('.');
upperBound = upperBound.split('.');
// A nice, classic for loop iterating over each segment in the IP address
for (var i = 0; i < 4; i++) {
// We need those segments to be converted to ints or our comparisons
// will not work!
ip[i] = parseInt(ip[i]);
lowerBound[i] = parseInt(lowerBound[i]);
upperBound[i] = parseInt(upperBound[i]);
// If this is our first iteration, just make sure the first segment
// falls within or equal to the range values
if (i === 0) {
if (ip[i] < lowerBound[i] || ip[i] > upperBound[i]) {
return false;
}
}
// If the last segment was equal to the corresponding lower bound
// segment, make sure that the current segment is greater
if (ip[i - 1] === lowerBound[i - 1]) {
if (ip[i] < lowerBound[i]) return false;
}
// If the last segment was equal to the corresponding upper bound
// segment, make sure that the current segment is less than
if (ip[i - 1] === upperBound[i - 1]) {
if (ip[i] > upperBound[i]) return false;
}
}
return true;
}