Google Sheets App Script Merge or Add New Row - google-apps-script

I have the following code which gets data from two sheets but can't figure out how update or add a new row in sheet "sumTransaction" where Category, Month & Year are equal.
For example in this example Expense 1, January, 2019 exists in the sumTransaction sheet so it should update the amount value by -3. Where Source A, January, 2019 and Other 1, March, 2019 do not exist in sumTransaction so they should be added to a new row.
function tableToObject() {
const ss = SpreadsheetApp.getActiveSpreadsheet()
const transactionSheet = ss.getSheetByName('Transactions')
const lastRow = transactionSheet.getLastRow()
const lastColumn = transactionSheet.getLastColumn()
const values = transactionSheet.getRange(1, 1, lastRow, lastColumn).getValues()
const [headers, ...originalData] = values.map(([,b,,d,e,,,,,,,,,,,p,q,r,s]) => [b,d,e,p,q,r,s])
const res = originalData.map(r => headers.reduce((o, h, j) => Object.assign(o, { [h]: r[j] }), {}))
console.log(res)
// GroupBy and Sum
const transactionGroup = [...res.reduce((r, o) => {
const key = o.Category + '_' + o.Month + '_' + o.Year
const item = r.get(key) || Object.assign({}, o, {
Amount: 0,
})
item.Amount += o.Amount
item.Key = key
return r.set(key, item)
}, new Map).values()]
console.log(transactionGroup)
const budgetValues = getBudget()
console.log(budgetValues)
// merge or add row
}
function getBudget(){
const ss = SpreadsheetApp.getActiveSpreadsheet()
const sumSheet = ss.getSheetByName('sumTransacation')
const lastRow = sumSheet.getLastRow()
const lastColumn = sumSheet.getLastColumn()
const values = sumSheet.getRange(1, 1, lastRow, lastColumn).getValues()
const [headers, ...originalData] = values.map(([a,b,c,d,e,f]) => [a,b,c,d,e,f])
const res = originalData.map(r => headers.reduce((o, h, j) => Object.assign(o, { [h]: r[j] }), {}))
return res
}
transactionGroup Data
[ { Date: Fri Jan 04 2019 00:00:00 GMT-0700 (Mountain Standard Time),
Category: 'Source A',
Amount: 85,
Month: 'January',
Year: 2019,
Group: 'COGS',
Debit: 'Credit',
Key: 'Source A_January_2019' },
{ Date: Mon Feb 25 2019 00:00:00 GMT-0700 (Mountain Standard Time),
Category: 'Expense 1',
Amount: -3,
Month: 'February',
Year: 2019,
Group: 'Expense',
Debit: 'Debit',
Key: 'Expense 1_February_2019' },
{ Date: Tue Mar 26 2019 00:00:00 GMT-0600 (Mountain Daylight Time),
Category: 'Other 1',
Amount: -4,
Month: 'March',
Year: 2019,
Group: 'Other',
Debit: 'Debit',
Key: 'Other 1_March_2019'
} ]
budgetValues Data
[ { Category: 'Expense 1',
Month: 'January',
Year: 2019,
Group: 'COGS',
Amount: 10,
'Debit/Credit': '' },
{ Category: 'Expense 2',
Month: 'January',
Year: 2019,
Group: 'COGS',
Amount: 10,
'Debit/Credit': '' } ]
Sample Image of sumTransactions Sheet ( i.e. before script )
Sample Image of sumTransactions Sheet ( i.e. after script )

I believe your goal is as follows.
There are 2 sheets which are the source sheet Transactions and the destination sheet sumTransacation.
You want to check the duplicated values between the source and destination sheets. At that time, you want to check the columns "A" to "C" on the destination sheet. So from your sample values, you want to check the values of Category, Month, and Year.
From your explanation,
When I saw your values of transactionGroup Data and budgetValues Data, Category: 'Expense 1' of transactionGroup Data is Month: 'February',. And Category: 'Expense 1' of budgetValues Data is Month: 'January',. When I saw the images of i.e. before script and i.e. after script, Category: 'Expense 1' of transactionGroup Data is removed. In this case, I thought that you might want to add the value of Category: 'Expense 1' of transactionGroup Data to the destination sheet.
When my understanding is correct, how about the following sample script?
Sample script:
I added the script's flow in the script as the comment.
function myFunction() {
const ss = SpreadsheetApp.getActiveSpreadsheet()
// 1. Retrieve values from destination sheet.
const dst = ss.getSheetByName('sumTransacation');
const [headers, ...dstVal] = dst.getDataRange().getValues();
// 2. Retrieve values from source sheet.
const src = ss.getSheetByName('Transactions');
const [srcHead, ...srcVal] = src.getDataRange().getValues();
const srcIdx = headers.reduce((ar, h) => {
const temp = srcHead.indexOf(h);
if (temp > -1) ar.push(temp);
return ar
}, []);
const srcValues = srcVal.map(r => srcIdx.map(i => r[i]));
// 3. Update values of destination sheet.
const obj1 = srcValues.reduce((o, r) => Object.assign(o, {[`${r[0] + r[1] + r[2]}`]: r}), {});
const values1 = dstVal.map(r => {
const temp = obj1[r[0] + r[1] + r[2]];
if (temp) {
return r.slice(0, 4).concat([r[4] + temp[4], r[5]]);
}
return r;
});
// 4. Added new values of source sheet.
const obj2 = dstVal.reduce((o, r) => Object.assign(o, {[`${r[0] + r[1] + r[2]}`]: r}), {});
const values2 = srcValues.reduce((ar, r) => {
if (!obj2[r[0] + r[1] + r[2]]) ar.push(r);
return ar;
}, []);
const values = [headers, ...values1, ...values2];
// 5. Update the destination sheet using new values.
dst.clearContents().getRange(1, 1, values.length, values[0].length).setValues(values);
}
References:
reduce()
map()
Edit:
When I saw your sample Spreadsheet, I noticed that your spreadsheet is different from your sample images. I think that this is the reason of your issue. So for your sample Spreadsheet, I added one more sample script as follows.
Sample script:
function sample2() {
const ss = SpreadsheetApp.getActiveSpreadsheet()
// 1. Retrieve values from destination sheet.
const dst = ss.getSheetByName('sumTransacation');
const [headers, ...dstVal] = dst.getDataRange().getValues();
// 2. Retrieve values from source sheet.
const src = ss.getSheetByName('Transactions');
const [srcHead, ...srcVal] = src.getDataRange().getValues().map(([,b,,d,e,,,,,,,,,,,p,q,r,s]) => [b,d,e,p,q,r,s])
const srcIdx = headers.reduce((ar, h) => {
const temp = srcHead.indexOf(h);
if (temp > -1) {
ar.push(temp);
} else {
ar.push("");
}
return ar
}, []);
const srcValues = srcVal.map(r => srcIdx.map(i => r[i]));
// 3. Update values of destination sheet.
const obj1 = srcValues.reduce((o, r) => Object.assign(o, {[`${r[0] + r[1] + r[2]}`]: r}), {});
const values1 = dstVal.map(r => {
const temp = obj1[r[0] + r[1] + r[2]];
if (temp) {
return r.slice(0, 4).concat([r[4] + temp[4], r[5]]);
}
return r;
});
// 4. Added new values of source sheet.
const obj2 = dstVal.reduce((o, r) => Object.assign(o, {[`${r[0] + r[1] + r[2]}`]: r}), {});
const values2 = srcValues.reduce((ar, r) => {
if (!obj2[r[0] + r[1] + r[2]]) ar.push(r);
return ar;
}, []);
const values = [headers, ...values1, ...values2];
dst.clearContents().getRange(1, 1, values.length, values[0].length).setValues(values);
}
In your sample Spreadsheet, the values of "Month" of "Transactions" is different from that of "sumTransacation" sheet. But, unfortunately, I cannot know your actual format. So, when you want to compare the values, how about changing the format as the same format? Please be careful this.

Related

Can't figure out how to add duplicate count to end of array

I think I'm totally overthinking this but I can't get out of my head. In Google Sheets, I have a list of work orders that look like this:
Work ID Location Start Time Officer Count
123 North 1100
123 North 1100
123 North 1100
222 South 1200
999 North 1400
999 North 1400
333 South 1200
Each work order always has one Officer assigned to it, so I need to count the duplicated work orders and push them to the end of the array under "Officer Count" so it shows how many total Officers needed. For example, 123 would need 3, 222 needs 1, 999 needs 2, and 333 needs 1.
However, the code I have now pops out the right count, just out of order from the original 2D array so I can't push it to the end of the array. Any suggestions?
var rowRange = sheet.getRange(2, 1, 7, 3).getValues();
//Create Work Number array (1D array)
var oneDArr = rowRange.map(function(row){return row[0];});
//Create object to count number of officers to Work Number
var counts = {};
oneDArr.forEach(function(x) { counts[x] = (counts[x] || 0)+1; });
//Create Array from Object
var array = [];
var array = Object.entries(counts);
//Set Values to correct location
// sheet.getRange(11, 1, array.length, array[0].length).setValues(array);
}
Description
Here is an example script to get the count of work orders and place them in the original array.
Code.gs
function test () {
try {
let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
let values = sheet.getDataRange().getValues();
values.shift(); // remove the headers
let ids = values.map( row => row[0] );
ids = [...new Set(ids)]; // create an array of unique ids
console.log(ids);
let count = ids.map( id => 0 ); // initialize count to 0
values.forEach( row => { let index = ids.indexOf(row[0]);
count[index]++;
});
console.log(count);
// now let put back into spreadsheet
ids.forEach( (id,i) => { let j = values.findIndex( row => row[0] === id );
values[j][3] = count[i]; } );
console.log(values);
}
catch(err) {
console.log(err);
}
}
Execution log
8:46:20 PM Notice Execution started
8:46:23 PM Info [ 123, 222, 999, 333 ]
8:46:23 PM Info [ 3, 1, 2, 1 ]
8:46:23 PM Info [ [ 123, 'North', 1100, 3 ],
[ 123, 'North', 1100, '' ],
[ 123, 'North', 1100, '' ],
[ 222, 'South', 1200, 1 ],
[ 999, 'North', 1400, 2 ],
[ 999, 'North', 1400, '' ],
[ 333, 'South', 1200, 1 ] ]
8:46:21 PM Notice Execution completed
References
Array.shift()
Array.map()
Set Object
Array.forEach()
Array.indexOf()
Array.findIndex()

Create a dynamic Mysql Insert from an Object

I have a project in Typescript in which I am trying to create an Insert through the options that I send through an object. Right now I have two objects, one for each Insert, these Inserts are created in different tables and with different objects. I would like to know if it is possible to create a general Insert for several objects.
This is what I currently have:
let object1 = [
{ country: 'CO', name: 'CO_SE.xml', exists: 1 },
{ country: 'CO', name: 'CO_IN.xml', exists: 1 },
{ country: 'CO', name: 'CO_BR.xml', exists: 1 }
];
`INSERT INTO ${database} VALUES` + object1.map((elem: any) =>
`"${elem.country}", "${elem.name}", ${elem.exists})`).join(', ');
let object2 = [
{ code: 1, folder: '/ToFtp', max: 8 },
{ code: 2, folder: '/ToXml', max: 5 },
{ code: 3, folder: '/ToMail', max: 5 }
];
`INSERT INTO ${database} VALUES` + object2.map((elem: any) =>
`${elem.code}, "${elem.folder}", ${elem.max})`).join(', ');
This is what I am trying to achieve:
let object1 = [
{ country: 'CO', name: 'CO_SE.xml', exists: 1 },
{ country: 'CO', name: 'CO_IN.xml', exists: 1 },
{ country: 'CO', name: 'CO_BR.xml', exists: 1 }
];
let object2 = [
{ code: 1, folder: '/ToFtp', max: 8 },
{ code: 2, folder: '/ToXml', max: 5 },
{ code: 3, folder: '/ToMail', max: 5 }
];
`INSERT INTO ${database} VALUES` + ${object}.map((elem: any) =>
`"${elem.elem1}", ... ${elem.elemN})`).join(', ');
Is this possible? I'm not sure this can be done.
You can create methods to prepare your query and values. You can do something like this.
let object1 = [
{ country: 'CO', name: 'CO_SE.xml', exists: 1 },
{ country: 'CO', name: 'CO_IN.xml', exists: 1 },
{ country: 'CO', name: 'CO_BR.xml', exists: 1 }
];
function getValues<T extends Record<string, any>[]>(obj: T) {
const values = obj.map((item) => {
const val = Object.values(item).map((v) => v);
return val;
});
return values;
}
function getColumns<T extends Record<string, any>>(obj: T) {
return Object.keys(obj);
}
function getFilters<T extends Record<string, any>>(obj: T) {
const constraints: string[] = [];
const queryArray: any[] = [];
Object.entries(obj).forEach(([k, v]) => {
constraints.push(`${k} = ?`);
queryArray.push(v);
});
return {
constraints,
queryArray
}
}
const columns = getColumns(object1[0]);
const values = getValues(object1);
const sql = `INSERT INTO MyTable(${columns.join(", ")}) VALUES ?`;
const filters = {
country: "CO"
};
const selectFilters = getFilters(filters);
const selectSql = `SELECT ${columns} FROM MyTable WHERE ${selectFilters.constraints.join(" AND ")}`;
console.log(columns); // [ 'country', 'name', 'exists' ]
console.log(sql); // INSERT INTO MyTable(country, name, exists) VALUES ?
console.log(values); // [ [ 'CO', 'CO_SE.xml', 1 ], [ 'CO', 'CO_IN.xml', 1 ], [ 'CO', 'CO_BR.xml', 1 ] ]
// now you can use something like this
connection.query({ sql, values });
Note: Make sure you sanitize your input before using these functions to avoid any damage you may create if your input is not as you are expecting it.

How to get cell value in automated mail via Google Sheets (& Script)

For some time I'm using a document in Google Sheets to fetch price data from a website. When a change is been made within our fetching sheet we receive an email notification (via Google Scripts). This works, but since there are a lot of changes per day I would like to get the cell location that has been changed within the email (so we know where to look directly). Does anyone know how I can change my script to also receive some data from the sheet itself (like the cell location that has been changed, the old cell data and the new cell data)? Thank you!
Script:
function sendEmailAlert() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var data = ss.getActiveSheet().getActiveCell().getA1Notation();
var sheetname = ss.getActiveSheet().getName();
var user = Session.getActiveUser().getEmail();
var Toemail = 'myemail#gmail.com';
var subject = 'New Entry in ' + data + '.' + ss.getName();
var body = 'Your file has a new entry in - ' + sheetname + ' Updated by - ' + user + data
' check file- ' + ss.getUrl();
if(data.indexOf('K2:K29')!=-1.23456789) {
MailApp.sendEmail(Toemail,subject, body);
}
};
Implementation depend on your workflow.
If you can get and store somewhere the old table and then get the new table you can compare them and get all the changes this way:
// old table
const old_table = [
['a1', 'b1', 'c1'],
['a2', 'b2', 'c2'],
['a3', 'b3', 'c3'],
['a4', 'b4', 'c4'],
];
// new table
const new_table = [
['a1', 'b1', 'c1'],
['a2', '🙂', 'c2'],
['🙂', 'b3', '🙃'],
['a4', 'b4', 'c4'],
];
const abc = 'ABCDEFGHIJKLMNOPQRTUVWXYZ';
// get indexes of changed rows
const changed_rows = old_table.map( (row, i) =>
(row.toString() != new_table[i].toString()) ? i : '' ).filter(String);
// get changes for every changed row
const changes = changed_rows.map( r =>
old_table[r].map( (col, i) =>
(col != new_table[r][i] ) ? {
'cell': abc[i] + (r+1),
'old_value': col,
'new_value': new_table[r][i]
} : '' ) .filter(String) ).filter(String).flat();
console.log(changes);
console.table(changes);
Output:
[
{
"cell": "B2",
"old_value": "b2",
"new_value": "🙂"
},
{
"cell": "A3",
"old_value": "a3",
"new_value": "🙂"
},
{
"cell": "C3",
"old_value": "c3",
"new_value": "🙃"
}
]
┌─────────┬──────┬───────────┬───────────┐
│ (index) │ cell │ old_value │ new_value │
├─────────┼──────┼───────────┼───────────┤
│ 0 │ 'B2' │ 'b2' │ '🙂' │
│ 1 │ 'A3' │ 'a3' │ '🙂' │
│ 2 │ 'C3' │ 'c3' │ '🙃' │
└─────────┴──────┴───────────┴───────────┘
Update
Here is another algorithm with the same output:
// old table
const OLD_TABLE = [
['a1', 'b1', 'c1'],
['a2', 'b2', 'c2'],
['a3', 'b3', 'c3'],
['a4', 'b4', 'c4'],
];
// new table
const NEW_TABLE = [
['a1', 'b1', 'c1'],
['a2', '🙂', 'c2'],
['🙂', 'b3', '🙃'],
['a4', 'b4', 'c4'],
];
function get_obj(row, col) {
var old_value = OLD_TABLE[row][col];
var new_value = NEW_TABLE[row][col];
if (old_value == new_value) return '';
return {
'cell': 'ABCDEFGHIJKLMNOPQRTUVWXYZ'[col] + (row+1),
'old value': old_value,
'new value': new_value
}
}
const changes = OLD_TABLE.map( (row, r) =>
row.map((_, c) => get_obj(r, c)) ).flat().filter(String);
console.log(changes);
console.table(changes);
Finally
Sorry, I can't help... :) One-liner is here:
const changes = (tab1, tab2) => tab1.map((row, r) => row.map((_, c) =>
tab1[r][c] == tab2[r][c] ? '' : ({
'cell' : 'ABCDEFGHIJKLMNOPQRTUVWXYZ'[c] + (r + 1),
'old_value' : tab1[r][c],
'new_value' : tab2[r][c] }))).flat(2).filter(String);
console.log(changes(OLD_TABLE, NEW_TABLE));
The same output.
Sure, just add whatever you like to the var body you have going. Here is some sample script for retrieving values from a sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var activeCell = sheet.getRange("AE13")
var currentCellValue = activeCell.getValue();
You would use the OnChange method
https://developers.google.com/apps-script/reference/script/spreadsheet-trigger-builder
Creating the trigger:
var sheet = SpreadsheetApp.getActive();
ScriptApp.newTrigger("myFunction")
.forSpreadsheet(sheet)
.onChange()
.create();

greater than and less than a date gives no records in sequelize but works with heidisql

It's really weird, I can't debug the issue. I also don't know what's causing the issue.
I have a query like below:
const sequelize = require('sequelize')
const Op = sequelize.Op
const TODAY_START = new Date().setHours(0, 0, 0, 0)
const NOW = new Date()
const data = await AssignedJob.findAll({
where: {
created_on: {
[Op.gt]: TODAY_START,
[Op.lt]: NOW
}
}
})
It generates a query like below.
SELECT `id`, `emp_id`, `zone_id`, `job_id`, `status`, `commission`, `rating`,
`created_by`, `updated_by`, `created_on`, `updated_on`
FROM `assigned_jobs` AS `AssignedJob`
WHERE (`AssignedJob`.`created_on` > '2020-03-24 00:00:00' AND `AssignedJob`.`created_on` < '2020-03-24 17:18:15');
But data is just an [] empty array.
I also tried using [Op.between]: [START_DATE, NOW], but still I didn't get any record.
I copied the same query to heidsql and ran it, I get the result there.
What's happening here? Can someone explain?
Data type of created_on and updated_on in sequelize is DATE, in the table it's TIMESTAMP
Use moment.js to format the date in 'YYYY-MM-DD HH:mm:ss'
const sequelize = require('sequelize')
const moment = require('moment');
const Op = sequelize.Op
function getDate(withoutTime) {
const date = new Date();
if (withoutTime) date.setHours(0, 0, 0, 0);
return moment(date).format('YYYY-MM-DD HH:mm:ss');
}
const TODAY_START = getDate(true); // '2020-03-24 00:00:00'
const NOW = getDate(); // '2020-03-24 17:47:41'
Problem const TODAY_START = new Date().setHours(0, 0, 0, 0) will result in Unix time i.e seconds after 1st Jan 1970
const date = new Date().setHours(0, 0, 0, 0)
console.log(date); // return seconds after 1970
const date1 = new Date();
date1.setHours(0, 0, 0, 0);
console.log(date1); // return date
I don't know the reason why that's happening, I got a fix for it. I have to use momentjs to achieve that.
const moment = require('moment')
const now = moment()
const todayAssignedJobs = await AssignedJob.findAll({
where: {
created_on: {
[Op.gt]: now.startOf('day').toString(),
[Op.lt]: now.endOf('day').toString()
},
status: 1
}
})
Same query is still being generated, but it gives result instead of giving an empty array.
SELECT `id`, `emp_id`, `zone_id`, `job_id`, `status`, `commission`, `rating`, `created_by`, `updated_by`, `created_on`, `updated_on` FROM
`assigned_jobs` AS `AssignedJob`
WHERE
(`AssignedJob`.`created_on` > '2020-03-24 00:00:00' AND `AssignedJob`.`created_on` <'2020-03-24 23:59:59')
AND `AssignedJob`.`status` = 1;
If someone got any explanation please comment or feel free to edit this answer.

How fix max number day in Month with Angular?

I have a date function with datepicker (with Angular 8), but I have a problem. for example when I enter 36/04/2020, it saves in the database with a date 05/06/2020.
I want when the number of days exceeds the number of days in a month it returns to me the last day (for example in my case it is necessary to enter 30/04/2020).
how to fix this and thank's ?
code .ts:
import { NativeDateAdapter, DateAdapter, MAT_DATE_FORMATS, MatDateFormats } from "#angular/material";
export class AppDateAdapter extends NativeDateAdapter {
parse(value: any): Date | null {
if ((typeof value === 'string') && (value.indexOf('/') > -1)) {
const str = value.split('/');
const year = Number(str[2]);
const month = Number(str[1]) - 1;
const date = Number(str[0]);
return new Date(Date.UTC(year, month, date));
;
}
const timestamp = typeof value === 'number' ? value : Date.parse(value);
return isNaN(timestamp) ? null : new Date(timestamp);
}
createDate(year: number, month: number, date: number): Date {
return new Date(Date.UTC(year, month, date));
}
format(date: Date, displayFormat: Object): string {
if (displayFormat === 'input') {
let day: string = date.getUTCDate().toString();
day = +day < 10 ? '0' + day : day;
let month: string = (date.getUTCMonth() + 1).toString();
month = +month < 10 ? '0' + month : month;
let year = date.getUTCFullYear();
return `${day}/${month}/${year}`;
}
// new Date(date.toDateString()).getUTCDate();
return date.toDateString();
}
private _to2digit(n: number) {
return (n);
//return ('00' + n).slice(-2);
}
}
export const APP_DATE_FORMATS =
{
parse: {
dateInput: {month: 'short', year: 'numeric', day: 'numeric'}
},
display: {
// dateInput: { month: 'short', year: 'numeric', day: 'numeric' },
dateInput: 'input',
// monthYearLabel: { month: 'short', year: 'numeric', day: 'numeric' },
monthYearLabel: 'inputMonth',
dateA11yLabel: {year: 'numeric', month: 'long', day: 'numeric'},
monthYearA11yLabel: {year: 'numeric', month: 'long'},
}
}
This will give you the last day for month =>
const day = new Date(2020, month + 1, 0)
Then to get date
day.getDate() it will return 31 for January
and finally compare
inputValue = inputValue > day.getDate() ? day.getDate() : inputValue