Linearize a BOM, issue when using setValues(result) instead of appendRow - google-apps-script

Input is as follows (this is a simplified example)
LEVEL
NAME
JOB
1
A
Alpha
2
B
Bravo
3
C
Charlie
4
D
Delta
2
E
Echo
3
F
Foxtrot
2
G
Golf
2
H
Hotel
3
I
India
4
J
Juliet
I have to linearize to obtain that output
NAME level 1
JOB level 1
NAME level 2
JOB level 2
NAME level 3
JOB level 3
NAME level 4
JOB level 4
A
Alpha
B
Bravo
C
Charlie
D
Delta
A
Alpha
E
Echo
F
Foxtrot
A
Alpha
G
Golf
A
Alpha
H
Hotel
I
India
J
Juliet
I achive that by using a temporary array (temp) and appendRow, which is a bit slow. When I manage to use a big array (result) and setValues(result), I only get the last row
NAME level 1
JOB level 1
NAME level 2
JOB level 2
NAME level 3
JOB level 3
NAME level 4
JOB level 4
A
Alpha
H
Hotel
I
India
J
Juliet
A
Alpha
H
Hotel
I
India
J
Juliet
A
Alpha
H
Hotel
I
India
J
Juliet
A
Alpha
H
Hotel
I
India
J
Juliet
I can't understand what is wrong in my script ! Any help to understand will be usefull.
https://docs.google.com/spreadsheets/d/1zoT9kk-Am_yUOLCAAvccJOTH0UZ7lrRiLYpPtqb9RXY/copy
function linearize() {
const sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Original')
const data = sh.getRange(2, 1, sh.getLastRow() - 1, sh.getLastColumn()).getDisplayValues()
const nbData = sh.getLastColumn() - 1
const bd1 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('BD1') // for test with appendrow
const bd2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('BD2') // for test with result
bd1.clearContents()
bd2.clearContents()
let result = []
let levelMax = 0
let headers = []
data.forEach(r => levelMax = Math.max(levelMax, r[0]))
for (let i = 1; i <= levelMax; i++) {
headers.push(['NAME level ' + i, 'JOB level ' + i])
}
bd1.appendRow(headers.flat())
result.push(headers.flat())
// everything ok until this step ==============
let temp = []
data.forEach(function (r, i) {
// save values
var level = r[0]
for (let x = 0; x < nbData; x++) {
temp[nbData * (level - 1) + x] = r[x + 1]
}
// blank values from level+1 to levelMax
if (level < levelMax) {
for (let y = (level * 1 + 1); y <= levelMax; y++) {
for (let x = 0; x < nbData; x++) {
temp[nbData * (y - 1) + x] = ''
}
}
}
// output when the following level will not increase or at the final row
if (i < data.length - 1) {
if (data[i + 1][0] <= data[i][0]) {
bd1.appendRow(temp)
result.push(temp)
}
}
else {
bd1.appendRow(temp)
result.push(temp)
}
})
bd2.getRange(1, 1, result.length, result[0].length).setValues(result)
}

I believe your goal is as follows.
By modifying your script, you want to achieve the situation of I have to linearize to obtain that output using bd2.getRange(1, 1, result.length, result[0].length).setValues(result).
In this case, how about the following modification?
From:
if (i < data.length - 1) {
if (data[i + 1][0] <= data[i][0]) {
bd1.appendRow(temp)
result.push(temp)
}
}
To:
if (i < data.length - 1) {
if (data[i + 1][0] <= data[i][0]) {
bd1.appendRow(temp);
result.push([...temp]); // <--- Modified. Or result.push(temp.slice())
}
}
I thought that in your script, temp is used as the pass-by-reference. By this, the issue of I only get the last row occurs. I thought that this might be the reason for your issue. So, in this case, temp is copied with [...temp] and/or temp.slice(). By this, it becomes the pass-by-value.

Related

Restarting a COUNTIF function depending on multiple criteria

I have a sheet with 4 columns, as shown below:
1
Date
Item Name
Counter
Flag
3
Date 1
Item A
1
4
Date 1
Item B
1
5
Date 2
Item B
2
6
Date 3
Item A
2
1
7
Date 3
Item B
3
8
Date 4
Item A
1
9
Date 5
Item A
2
Currently, I'm using a countif function [=countif(B$2:B2,B2)] to count the number of times a specific item appears in the spreadsheet. However, I need to find a way to restart the counter if there is a 1 in column D. In this case, this would mean that the formula in row 8 column C would be [=COUNTIF(B$8:B8,B8)] and would continue counting until it finds another row with a 1 in column D (e.g., formula in column C row 9 would be =COUNTIF(B$8:B9,B9). It would also ideally check whether there is a prior row with a 1 in column D, not through the order of the sheet, but by checking that it's date is smaller (and yet the closest date with a 1 in column D).
I've written the following script, which sets the row with a 1 in column D to 0 and sets the countif for the starting rows correctly to [=countif(B$2:B2,B2)], but it sets any row after there is a row with a 1 in column D as the same formula, with the starting range at B$2.
function setCountifFormula() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Test");
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) { //iterate through each row
var colBValue = data[i][1]; //get columnB in i
var colAValue = data[i][0]; // get date in i
var colDValue = data[i][3]; // get flag in i
var closestRow = 1; // empty variable
if( colDValue == "1") { //if columnD = 1
sheet.getRange(i+1,3).setValue(0); // set columnC = 0
} else {
for (var j = 1; j < data.length; j++) { //iterate through other rows
if (data[j][1] === colBValue && data[j][3] === "1") { // if columnB in j = ColumnB in i, and flag in row j = 1
var dateToCompare = data[j][0]; //set datetoCompare = date in row j
closestRow = j;
if (dateToCompare < colAValue) {
var range = "B$" + (closestRow + 1) + ":B" + (i + 1);
var formula = "=COUNTIF(" + range + ",B" + (i + 1) + ")";
sheet.getRange(i + 1, 3).setFormula(formula);
} else {
var range = "B$2:B" + (i+1);
var formula = "=COUNTIF(" + range + ",B" + (i+1) + ")";
sheet.getRange(i+1, 3).setFormula(formula);
}
}
}
if (closestRow === 1) {
var range = "B$2:B" +(i+1);
var formula = "=COUNTIF("+range +",B"+(i+1)+")";
sheet.getRange(i+1,3).setFormula(formula);
}
}
}
}
I can post the spreadsheet if needs be. If there is a different way without using scripts or COUNTIF, it'd be appreciated. Thanks!
I'm much better at scripting than complex formulas so here is an example of how I would do it.
function myCountif() {
try {
let values = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
values.shift(); // remove headers
let unique = values.map( row => row[1] );
unique = [...new Set(unique)];
let count = unique.map( row => 0 );
let counts = values.map( row => 0 );
values.forEach( (row,rIndex) => {
let cIndex = unique.findIndex( item => item === row[1] );
count[cIndex] = count[cIndex]+1;
counts[rIndex] = count[cIndex];
if( row[3] === 1 ) count[cIndex] = 0;
}
)
return counts;
}
catch(err) {
console.log(err);
}
}
Reference
Array.shift()
Array.map()
Set Object
Array.forEach()
Array.findIndex()
Arrow function =>

Applying my function to every position in a Numpy array

I am attempting to apply my CasinoTime function to each position in my array per sweep. The function currently does this but chooses a position at random and can only do one position per sweep. The output looks something like this:
Starting Configuration: [[ 1 -1 1 1 -1 -1 1 1 1 -1]]
0: [[ 1 -1 1 1 -1 -1 1 1 1 -1]] Energy: [-2] Spin: 0.2
1: [[ 1 -1 1 1 1 -1 1 1 1 -1]] Energy: [[-10]] Spin: 0.4
2: [[ 1 -1 1 1 1 -1 1 1 1 -1]] Energy: [[-10]] Spin: 0.4
3: [[-1 -1 1 1 1 -1 1 1 1 -1]] Energy: [[-2]] Spin: 0.2
As you can see, it only alters one position at a time. Ideally my function will run through every position in my array (per sweep) and change the values which meet the criteria. This is a simulation of A Monte Carlo/Metropolis Algorithm. I have attempted to use a map function to apply my function to my array however this does not seem to work. Any help is greatly appreciated and I have attached my code below!
import numpy as np
N = 10
J = 1
H = 2
def calcEnergy(config, J, H):
energy = 0
for i in range(config.size):
spin = config[i]
neighbour = config[(i + 1) % N]
energy = energy - J * (spin * neighbour) - H * neighbour
return energy
def ChangeInEnergy(J, H, spin, spinleft, spinright):
dE = 2 * H * spin + 2 * J * spin * (spinleft + spinright)
return dE
def CasinoTime(sweeps, beta, J, H, debug=False):
config = np.random.choice([-1, 1], (N, 1))
averagespins = []
if debug is True:
print("Starting Configuration:", (np.transpose(config)))
runningenergy = calcEnergy(config, J, H)
for i in range(sweeps):
spinlocation = np.random.randint(N)
spin = config[spinlocation]
spinright = config[(spin + 1) % N]
spinleft = config[(spin - 1) % N]
dE = ChangeInEnergy(J, H, spin, spinleft, spinright)
r = np.random.random()
if r < min(1, np.exp(-beta * dE)):
config[spinlocation] *= -1
runningenergy = runningenergy + dE
else:
pass
averagespin = config.mean()
if debug and i % 1 == 0:
print("%i: " % i, np.transpose(config), "Energy:", runningenergy, "Spin:", averagespin)
return averagespins
averagespins = CasinoTime(sweeps=20, beta=0.1, J=1, H=2, debug=True)

Finding matches from 2 ranges and replacing matches with a specific value, any way to optimize?

This is the sample sheet.
From this
1 Search area Bounty list Bullet
2 a i z a b c abc
3 e b d d e f def
4 y f h g h i ghi
5
6 1 2 3 4 5 6 7 8
7 Column #
To this
1 Search area Bounty list Bullet
2 abc ghi z a b c abc
3 def abc def d e f def
4 y def ghi g h i ghi
5
6 1 2 3 4 5 6 7 8
7 Column #
It will take a value "bounty" from the "bounty list" starting from (2,5) or "a", search around the "Search Area" in a sequence, from a, i, z, e, b, d, y, f, h. Then if it finds a cell or multiple cells that equals the value of "bounty", then it will place the value of "bullet" from column 8 on the current "bounty" row to those cells. The process will repeat in the sequence of a, b, c, d, e, f, g, h, i on the "bounty list". Both process moves to the left, and down.
function menuItem1()
{
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var target = sheet.getDataRange().getValues();
for (var BountyRow = 2; BountyRow<target.length; BountyRow++)//switching rows in bounty list//
{
var bullet = sheet.getRange(BountyRow, 8).getValue(); //cell value to paste on targets//
for (var BountyColumn = 5; BountyColumn<8; BountyColumn++) //switching columns in bounty list//
{
var bounty = sheet.getRange(BountyRow, BountyColumn).getValue(); // cell value to search for//
if (bounty !=0)
{
for (var SearchRow = 1; SearchRow<target.length; SearchRow++) //switching row on search area//
{
for(var SearchColumn = 0; SearchColumn<4;SearchColumn++)//switching column on search area//
{
if(target[SearchRow][SearchColumn] == bounty) //if search target is found//
{
var found = target[SearchRow][SearchColumn];
sheet.getRange(SearchRow+1, SearchColumn+1).setValue(bullet);
Logger.log((found)+ " in "+"row"+(SearchRow+1)+", column"+(SearchColumn+1));
}
}
}
}
}
}
}
It involves thousands of searches which always use more than a minute and I was wondering if there is a more efficient way to do it?
In order to optimize your code you need to do two things:
Instead of using getValue() and setValue() for each cell (which makes you code slow)
retrieve all your bounty list and search are data once, with getValues()
assign the values to an array
replace matches within the the array
set the updated array values back into the range with setValues()
Make use of indexOf() and map()
to find matches and replace them more efficiently
Sample:
function menuItem1(){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var lastRow=sheet.getLastRow();
var searchValues=sheet.getRange(2,1,lastRow-2+1,3).getValues();
var bountyValues=sheet.getRange(2,5,lastRow-2+1,3).getValues();
var bulletValues=sheet.getRange(2,8,lastRow-2+1,1).getValues();
for (var i = 0; i<bountyValues.length; i++){
for (var j = 0; j<bountyValues[0].length; j++){
if (bountyValues[i][j] !=0){
replaceValues(searchValues, bountyValues[i][j], bulletValues[i][0]);
}
}
}
sheet.getRange(2,1,lastRow-2+1,3).setValues(searchValues)
}
function replaceValues(search, bounty, bullet) {
for(var k=0;k<search.length;k++){
search[k]=search[k].map(function(search) {
var regex=new RegExp("\\b"+bounty+"\\b","g");
return search.toString().replace(regex, bullet);
});
}
}

How to getRange in all cell appear when reshape data for wide to long form by monthly columns name GooglespreadSheet

In R , data.table library dcast() can transform dataset from wide to long shape ,how can i do this in googlespreadsheet?
Sheet1
Name Type YTD JAN FEB MAR
Product 1 A 8 1 3 4
Product 2 B 519 41 23 455
Product 3 C 32 2 25 5
NA D 3 NA 2 1
Sheet2
A B C D E F
1 Name Type YTD JAN FEB MAR
2 =filter(Sheet1!A2:F5,not(isblank(Sheet1!A2:A5)))
Show reshaped data in Sheet3
from A1
[
** C column for YTD is not necessarily needed .
Adjusted script by me not works :
from Tanaike
function myFunction() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var values = sheet.getRange(1,1,sheet.getLastRow(),sheet.getLastColumn()).getValues(); // Retrieve values
var Result_sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet2");
var result = [];
for (var i = 1; i < values.length; i++) {
for (var j = 1; j < values[i].length; j++) {
result.push([values[0][i], values[j][0], values[j][i]]);
}
}
Result_sheet.getRange().setValues(result); // Put result
}
I am too new to java script that cannot tell the reason.
#J. G. had the right idea, but there are a couple bugs in that code (e.g. i and j transposed) which resulted in the error #rane commented on.
This should do what the o.p. asked for without the need for filtering as in the Sheet2 intermediate step.
function myFunction() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1"); // identify source sheet
var values = sheet.getDataRange().getValues(); // Retrieve values
var Result_sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet2");
var result = [];
result.push(["Name","Type","Date","YTD","QTY"]);
for (var i = 1; i < values.length; i++) {
for (var j = 3; j < values[i].length; j++) {
if (values[i][j].length < 1) continue; // skip columns where 'qty' is blank, remove this line if you want empty value rows
if (values[i][0].length < 1) continue; // skip rows where 'Name' is blank
result.push([ values[i][0], values[i][1], values[0][j], values[i][2], values[i][j] ]);
}
}
Result_sheet.getRange(1,1,result.length, result[0].length).setValues(result); // Populate results
}
which results in...
Name Type Date YTD QTY
Product 1 A JAN 8 1
Product 1 A FEB 8 3
Product 1 A MAR 8 4
Product 2 B JAN 519 41
Product 2 B FEB 519 23
Product 2 B MAR 519 455
Product 3 C JAN 32 2
Product 3 C FEB 32 25
Product 3 C MAR 32 5
Ok, so from what I understand you want to take the first table, remove blank products, and decompress it.
The reason tnaike's script didn't work for you is that you have two leader columns (product and type) and not just one. This appears to adjust everything correctly.
function myFunction() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var values = sheet.getDataRange().getValues(); // Retrieve values
var Result_sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet2");
var result = [];
result.push(["Name","Type","Month","QTY"]);
for (var i = 1; i < values.length; i++) {
for (var j = 1; j < values[i].length; j++) {
if (values[j][0].length < 1) continue;
if (values[0][i].length < 1) continue;
result.push([ values[j][0],values[j][1], values[0][i], values[j][i]]);
}
}
Result_sheet.getRange(1,1,result.length, result[0].length).setValues(result); // Put result
Result_sheet.sort(1);
}
this results in:
Name/Type/Month/QTY
P1 A Ja 1
P1 A Fe 3
P1 A Ma 4
P2 B Ja 41
P2 B Fe 23
P2 B Ma 455
P3 C Ja 2
P3 C Fe 25
P3 C Ma 5

any one knows where I could find (or I could make) a function that calculate moon phase ? (AS3)

I'm looking for a small fucntion in ActionScript 3 that could calculate Moon Phases.
I've tried to search on google. The only result is a website that gives this code but I think it's a wrong code.
//return frame number for moon phase display
function getMoonPhase(yr:Number, m:Number, d:Number):int{
//based on http://www.voidware.com/moon_phase.htm
//calculates the moon phase (frames 1-30 )
if (m < 3) { yr -= 1; m += 12; } //
m += 1;
var c:Number = 365.25*yr;
var e:Number = 30.6*m; //jd is total days elapsed
//divide by the moon cycle (29.53 days)
var jd:Number = (c+e+d-694039.09)/29.53; //subtract integer to leave fractional part
jd = jd - int(jd); //range fraction from 0-30 and round by adding 0.5
var frame:int = Math.round(jd*30 + 0.5);
return frame;
}
//test: september 23, 2002, not a full moon? //
Weirdly, sometimes the code is extremely accurate, but sometimes it's wrong..
Example : On the 16 september 2016 it's a full moon.
But if I enter this date in the code the result is "15" (16 is full moon)....
Any idea why ? or another way to calculate moon phases ?
Thx
In that algorithm, c is int, not Number, therefore an implicit round-down occurs here. Same with e. This can total to a one day error in calculation which in turn produces wrong phase.
Edit: Also since you request a 30-frame selection, but have a precision of 29, the calculation can skip a frame. Consider using 15 or 16 frames instead.
function julday(year, month, day) {
if (year < 0) { year ++; }
var jy = parseInt(year);
var jm = parseInt(month) +1;
if (month <= 2) {jy--; jm += 12; }
var jul = Math.floor(365.25 *jy) + Math.floor(30.6001 * jm) + parseInt(day) + 1720995;
if (day+31*(month+12*year) >= (15+31*(10+12*1582))) {
var ja = Math.floor(0.01 * jy);
jul = jul + 2 - ja + Math.floor(0.25 * ja);
}
return jul;
}
function moonphase(year,month,day) {
var n = Math.floor(12.37 * (year -1900 + ((1.0 * month - 0.5)/12.0)));
var RAD = 3.14159265/180.0;
var t = n / 1236.85;
var t2 = t * t;
var aas = 359.2242 + 29.105356 * n;
var am = 306.0253 + 385.816918 * n + 0.010730 * t2;
var xtra = 0.75933 + 1.53058868 * n + ((1.178e-4) - (1.55e-7) * t) * t2;
xtra += (0.1734 - 3.93e-4 * t) * Math.sin(RAD * aas) - 0.4068 * Math.sin(RAD * am);
var i = (xtra > 0.0 ? Math.floor(xtra) : Math.ceil(xtra - 1.0));
var j1 = julday(year,month,day);
var jd = (2415020 + 28 * n) + i;
return (j1-jd + 30)%30;
}
not that i fully understand it, but this should work fine. It's a (straight) port of some Basic code by Roger W. Sinnot from Sky & Telescope magazine, March 1985 ... talk about old school.
if you try with 16 sept 2016 it will give 15. same as 21 sept 2002, which was the actual day of the fullmoon