I have a custom formula for conditional formatting rules. I am trying to write a script that checks a number of values (around 50) on a column (column B on 'Mine' sheet) and if a cell is equal to a specific string (M1, M2 or M3) then the specified formula for conditional formatting is applied to the "Calendar view" sheet. The code I currently have is:
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Calendar View");
sheet.getRange("C4:NC50").clearFormat();
var range = sheet.getRange("C4:NC4");
var rule = SpreadsheetApp.newConditionalFormatRule()
.whenFormulaSatisfied('=AND(indirect("Mine!$B5")="M1", C$2>=indirect("Mine!$C5"), C$2<indirect("Mine!$D5"))')
.setBackground("#FF0000")
.setRanges([range])
.build();
var rules = sheet.getConditionalFormatRules();
rules.push(rule);
sheet.setConditionalFormatRules(rules);
}
How can I enter an iteration method on the .whenFormulaSatisfied, such as:
.whenFormulaSatisfied('=AND(indirect("Mine!$B6")="M1", C$2>=indirect("Mine!$C6"), C$2<indirect("Mine!$D6"))')
.whenFormulaSatisfied('=AND(indirect("Mine!$B7")="M1", C$2>=indirect("Mine!$C7"), C$2<indirect("Mine!$D7"))')
.whenFormulaSatisfied('=AND(indirect("Mine!$B8")="M1", C$2>=indirect("Mine!$C8"), C$2<indirect("Mine!$D8"))')
............
This is the sheet I'm working on:
https://docs.google.com/spreadsheets/d/1Af84aHaG0VjXmtaWc0-uAdGFrX1LozRNLQLMatSOqgU/edit?usp=sharing
There are some challenges to the questioner's methodology - first, the dynamic identification of the start and end dates for each property, and second, the creation of up to 50 separate Conditional Formatting rules. It's well known that spreadsheet performance is affected by numbers of Conditional Formatting rules.
I'm suggesting a slightly different approach.
1) Take the data on Mine and build the Calendar.
2) Place values in the booked date fields.
3) Apply a single Conditional Formatting rule for Calendar.
The methodology for identifying which dates are booked is to insert a nominal value in the respective cells. Then the rule .whenCellNotEmpty() is applied rather than specifying a specific value. In addition, the code formats both the background as well as the font colour so that any data is hidden.
Also to note: at the beginning of the script, the code removes both the content as well as the formatting.
function so_53185335() {
// build the spreadsheet app and set source and target sheets
var ss = SpreadsheetApp.getActiveSpreadsheet();
var calSheet = ss.getSheetByName("Calendar View");
var dataSheet = ss.getSheetByName("Mine");
// get the last rows and start rows for both sheets
var lrMine = dataSheet.getLastRow();
var lrCal = calSheet.getLastRow();
var dataRowStart = 5;
var calRowStart = 4;
// clear formats and data from Calendar
calSheet.getRange(calRowStart, 2, lrCal, 366).clear({
contentsOnly: true,
formatOnly: true
});
// get Mine rows with data, define the range and get data
var dataRows = lrMine - dataRowStart + 1;
//Logger.log("Mine: number of data rows "+dataRows);// DEBUG
var dataRange = dataSheet.getRange(dataRowStart, 2, dataRows, 3);
//Logger.log("data range is "+dataRange.getA1Notation());// DEBUG
var dataValues = dataRange.getValues();
//set some variables for use in loop
var i = 0; // counter
var z = 0; // counter
var calstartCol = 3; // equals first day of the year
var calrow = 0; // counter row for Calendar sheet
var calArray = [];
var masterArray = [];
// loop through the rows in Mine
for (i = 0; i < dataRows; i++) {
// test for value
if (dataValues[i][0] === "M1" || dataValues[i][0] === "M2" || dataValues[i][0] === "M3") {
//Logger.log("Match: i="+i+", value = "+dataValues[i][0]);//DEBUG
calArray = [];
masterArray = [];
calrow = calrow + 1;
// calculate the start day (as a day in the year)
var now = new Date(dataValues[i][1]);
var start = new Date(now.getFullYear(), 0, 0);
var diff = (now - start) + ((start.getTimezoneOffset() - now.getTimezoneOffset()) * 60 * 1000);
var oneDay = 1000 * 60 * 60 * 24;
var startday = Math.floor(diff / oneDay);
// calculate the end day (as a day in the year)
var fnow = new Date(dataValues[i][2]);
var fstart = new Date(fnow.getFullYear(), 0, 0);
var fdiff = (fnow - fstart) + ((fstart.getTimezoneOffset() - fnow.getTimezoneOffset()) * 60 * 1000);
var foneDay = 1000 * 60 * 60 * 24;
var endday = Math.floor(fdiff / foneDay);
var nod = endday - startday + 1;
// assign the value for the Property
var cell = calSheet.getRange(calstartCol + calrow, 2);
cell.setValue(dataValues[i][0]);
// create an array of values for booked dates; just insert the number "1"
for (z = 1; z < nod + 1; z++) {
calArray.push(1);
}
masterArray.push(calArray);
// Assign the values for booked dates
var cell = calSheet.getRange(calstartCol + calrow, startday + 2, 1, nod);
cell.setValue(masterArray);
}
}
// create and apply a single Conditional forma rule for the data range on Calendar
var range = calSheet.getRange(calRowStart, calstartCol, calstartCol + calrow, 366);
var rule = SpreadsheetApp.newConditionalFormatRule()
.whenCellNotEmpty()
.setFontColor("#FF0000")
.setBackground("#FF0000")
.setRanges([range])
.build();
var rules = calSheet.getConditionalFormatRules();
rules.push(rule);
calSheet.setConditionalFormatRules(rules);
}
The Calendar looks like this.
I found two scripts. Since they both were an onEdit() function they couldn't work side by side, I tried to merge them.
I am using this sheet for a very basic inventory of things I sell / keep track of what I have sold.
I want two things;
First: I would like the cell in column A to change to the current date whenever I do a change on that row.
Second: If I change the value in column B to "Sold" I want it to be moved to a different sheet (as well as getting a new date due the change).
Column B has the following choices:
-Ja (as in stock)
-Bokad (as in booked)
-Såld (as in sold)
The name of the Sheets are:
-Blocket (inventory)
-Sålda (sold)
function onEdit(event) {
// assumes source data in sheet named Blocket
// target sheet of move to named Sålda
// test column with "Såld" is col 2
var s = event.source.getActiveSheet(),
cols = [2],
colStamp = 1,
ind = cols.indexOf(event.range.columnStart)
if (s.getName() == 'Blocket' || ind == -1) return;
event.range.offset(0, parseInt(colStamp - cols[ind]))
.setValue(e.value ? new Date() : null);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = event.source.getActiveSheet();
var r = event.source.getActiveRange();
if(s.getName() == "Blocket" && r.getColumn() == 2 && r.getValue() == "Såld")
{
var row = r.getRow();
var numColumns = s.getLastColumn();
var targetSheet = ss.getSheetByName("Sålda");
if(targetSheet.getLastRow() == targetSheet.getMaxRows()) {
targetSheet.insertRowsAfter(targetSheet.getLastRow(), 20); //inserts 20 rows
after last used row
}
var target = targetSheet.getRange(targetSheet.getLastRow() + 1, 1);
s.getRange(row, 1, 1, numColumns).moveTo(target);
s.deleteRow(row);
}
}
See if this works
function onEdit(e) {
var s, targetSheet;
s = e.source.getActiveSheet();
if (s.getName() !== 'Blocket' || e.range.columnStart == 1) return;
s.getRange(e.range.rowStart, 1)
.setValue(new Date());
if (e.range.columnStart == 2 && e.value == "Såld") {
targetSheet = e.source.getSheetByName("Sålda");
if (targetSheet.getLastRow() == targetSheet.getMaxRows()) {
targetSheet.insertRowsAfter(targetSheet.getLastRow(), 20); //inserts 20 rows
}
s.getRange(e.range.rowStart, 1, 1, s.getLastColumn()).moveTo(targetSheet.getRange(targetSheet.getLastRow() + 1, 1));
}
}
I have too many columns in the sheet and so many columns to hide as well, but while executing the script it runs half way and stopped saying maximum time reached.
When I again tried to execute it stopped exactly where I stopped previously. So I would like to have some customization that if the column is already hidden can skip that column and work on the others.
Is there any way to do it.
Here is the code I used:
function hideColumns() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('ANALYTIC');
var data = sh.getRange('6:6').getValues();
var numCols = sh.getMaxColumns();
var numRows = sh.getMaxRows();
for(var i = 0; i <= numCols; i++){
if(data[0][i] == ""){
sh.hideColumns(i+1);
} else {
sh.unhideColumn(sh.getRange(1, i+1, numRows, 1));
}
}
}
Please help me.
You can use the documentProperties to store the last column before the end of the execution. To prevent the run from stopping abruptly you stop the run a little prematurely at 5min (execution will terminate at 6min mark) mark and store the column number in the documentProperty. You also display an alert asking you rerun the script.
Then retrieve the column number on the next run and start from there. If the program gets through the complete loop you delete the said properties. So you start from zero if you rerun the script next time.
Below is the code for the same
function hideColumns() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('ANALYTIC');
var data = sh.getRange('6:6').getValues();
var numCols = sh.getMaxColumns();
var numRows = sh.getMaxRows();
var docProp = PropertiesService.getDocumentProperties()
var startCol = Number(docProp.getProperty("startCol")) //if there is no propert called startCol will return Null, Number(Null) = 0
Logger.log(startCol)
var startTime = new Date().getTime()
var ms5min = 5*60*1000 //5min in millseconds
for(var i = startCol; i <= numCols; i++){
if(data[0][i] == ""){
sh.hideColumns(i+1);
} else {
sh.unhideColumn(sh.getRange(1, i+1, numRows, 1));
}
var curTime = new Date().getTime()
var elasped = curTime-startTime
if (elasped >= ms5min){
docProp.setProperty("startCol", i)
SpreadsheetApp.getUi().alert("Please restart Run, exceeded 5min mark")
return
}
}
Logger.log(elasped)
docProp.deleteAllProperties()
}
If your sheet has under 10,000 columns (PropertiesService limit) you can use this script:
function hideColumns() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('ANALYTIC');
var data = sh.getRange('6:6').getValues();
var documentProperties = PropertiesService.getDocumentProperties()
var documentPropertiesVals = documentProperties.getProperties();
var numCols = sh.getMaxColumns();
for(var i = 0; i < numCols; i++){
if (!(cPlusInt(i) in documentPropertiesVals)) {
documentPropertiesVals[cPlusInt(i)] === 'empty';
}
if (documentPropertiesVals[cPlusInt(i)] === 'hidden' && data[0][i] == "") continue;
if (documentPropertiesVals[cPlusInt(i)] === 'unhidden' && data[0][i] != "") continue;
if(data[0][i] == ""){
sh.hideColumns(i+1);
documentProperties.setProperty(cPlusInt(i), 'hidden')
} else {
sh.unhideColumn(sh.getRange(1, i+1, 1, 1));
documentProperties.setProperty(cPlusInt(i), 'unhidden')
}
}
}
function cPlusInt(num) {
return 'c'+num.toString()
}
You at first you may need to run this few times (many write operation to PropertiesService may be sluggish) but later it's "blazingly" fast (0.1 s per new column).
Better answer using #Jack Brown's idea
It is possible to make single save to PropertiesService - you would need to incorporate time limit from #Jack Brown's answer, this way:
function hideColumns() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('ANALYTIC');
var data = sh.getRange('6:6').getValues();
var documentProperties = PropertiesService.getDocumentProperties()
var documentPropertiesVals = documentProperties.getProperties();
var numCols = sh.getMaxColumns();
var startTime = new Date().getTime()
var ms5min = 5*60*1000 //5min in millseconds
for(var i = 0; i < numCols; i++){
if (!(cPlusInt(i) in documentPropertiesVals)) {
documentPropertiesVals[cPlusInt(i)] === 'empty';
}
if (documentPropertiesVals[cPlusInt(i)] === 'hidden' && data[0][i] == "") continue;
if (documentPropertiesVals[cPlusInt(i)] === 'unhidden' && data[0][i] != "") continue;
if(data[0][i] == ""){
sh.hideColumns(i+1);
documentPropertiesVals[cPlusInt(i)] = 'hidden'
} else {
sh.unhideColumn(sh.getRange(1, i+1, 1, 1));
documentPropertiesVals[cPlusInt(i)] = 'unhidden'
}
var curTime = new Date().getTime()
var elapsed = curTime-startTime
if (elapsed >= ms5min){
break;
}
}
documentProperties.setProperties(documentPropertiesVals)
if (elapsed >= ms5min){
SpreadsheetApp.getUi().alert("Please restart Run, exceeded 5min mark")
}
}
cPlusInt function explanation
cPlusInt is necessary because of weird problems with google's PropertiesService. Object gotten from PropertiesService returns undefined at integer keys. To see problem use this code:
function test() {
var obj = {};
for (var i=0; i<10; i++) {
obj[i.toString()] = 'aa' + i.toString();
}
var documentProperties = PropertiesService.getScriptProperties();
documentProperties.deleteAllProperties();
documentProperties.setProperties(obj);
obj = documentProperties.getProperties();
Logger.log(obj)
for (var i in obj) {
Logger.log('%s %s %s', i, obj[i], i === '1');
}
}
I am trying to set up an AdWords script for the first time but cannot get it to function properly. It is essentially supposed to crawl all of our clients' AdWords accounts, send all of the account information to a Google Sheet, and send me an email to let me know if there are any anomalies detected in any of the accounts. I have not had any luck with getting the info to send to the Google sheet, let a lone an email notification. This is the primary error I'm currently getting when I preview the script: TypeError: Cannot call method "getValue" of null. (line 510)
Here is the Google resource page (https://developers.google.com/adwords/scripts/docs/solutions/mccapp-account-anomaly-detector) for the script and the actual script itself that I'm using is below.
Any recommendations on how to get this to function properly would be greatly appreciated. Thank you!
// Copyright 2017, Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* #name MCC Account Anomaly Detector
*
* #fileoverview The MCC Account Anomaly Detector alerts the advertiser whenever
* one or more accounts in a group of advertiser accounts under an MCC account
* is suddenly behaving too differently from what's historically observed. See
* https://developers.google.com/adwords/scripts/docs/solutions/mccapp-account-anomaly-detector
* for more details.
*
* #author AdWords Scripts Team [adwords-scripts#googlegroups.com]
*
* #version 1.4
*
* #changelog
* - version 1.4
* - Added conversions to tracked statistics.
* - version 1.3.2
* - Added validation for external spreadsheet setup.
* - version 1.3.1
* - Improvements to time zone handling.
* - version 1.3
* - Cleanup the script a bit for easier debugging and maintenance.
* - version 1.2
* - Added AdWords API report version.
* - version 1.1
* - Fix the script to work in accounts where there is no stats.
* - version 1.0
* - Released initial version.
*/
var SPREADSHEET_URL = 'https://docs.google.com/a/altitudemarketing.com/spreadsheets/d/1ELWZPcGLqf7n9GDnTx5o7xWOFZHVbgaLakeXAu5NY-E/edit?usp=sharing';
var CONFIG = {
// Uncomment below to include an account label filter
// ACCOUNT_LABEL: 'High Spend Accounts'
};
var CONST = {
FIRST_DATA_ROW: 12,
FIRST_DATA_COLUMN: 2,
MCC_CHILD_ACCOUNT_LIMIT: 50,
TOTAL_DATA_COLUMNS: 9
};
var STATS = {
'NumOfColumns': 4,
'Impressions':
{'Column': 3, 'Color': 'red', 'AlertRange': 'impressions_alert'},
'Clicks': {'Column': 4, 'Color': 'orange', 'AlertRange': 'clicks_alert'},
'Conversions':
{'Column': 5, 'Color': 'dark yellow 2', 'AlertRange': 'conversions_alert'},
'Cost': {'Column': 6, 'Color': 'yellow', 'AlertRange': 'cost_alert'}
};
var DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
'Saturday', 'Sunday'];
/**
* Configuration to be used for running reports.
*/
var REPORTING_OPTIONS = {
// Comment out the following line to default to the latest reporting version.
apiVersion: 'v201605'
};
function main() {
var account;
var alertText = [];
Logger.log('Using spreadsheet - %s.', SPREADSHEET_URL);
var spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
spreadsheet.setSpreadsheetTimeZone(AdWordsApp.currentAccount().getTimeZone());
var dataRow = CONST.FIRST_DATA_ROW;
SheetUtil.setupData(spreadsheet);
Logger.log('MCC account: ' + mccManager.mccAccount().getCustomerId());
while (account = mccManager.next()) {
Logger.log('Processing account ' + account.getCustomerId());
alertText.push(processAccount(account, spreadsheet, dataRow));
dataRow++;
}
sendEmail(mccManager.mccAccount(), alertText, spreadsheet);
}
/**
* For each of Impressions, Clicks, Conversions, and Cost, check to see if the
* values are out of range. If they are, and no alert has been set in the
* spreadsheet, then 1) Add text to the email, and 2) Add coloring to the cells
* corresponding to the statistic.
*
* #return {string} the next piece of the alert text to include in the email.
*/
function processAccount(account, spreadsheet, startingRow) {
var sheet = spreadsheet.getSheets()[0];
var thresholds = SheetUtil.thresholds();
var today = AdWordsApp.report(SheetUtil.getTodayQuery(), REPORTING_OPTIONS);
var past = AdWordsApp.report(SheetUtil.getPastQuery(), REPORTING_OPTIONS);
var hours = SheetUtil.hourOfDay();
var todayStats = accumulateRows(today.rows(), hours, 1); // just one week
var pastStats = accumulateRows(past.rows(), hours, SheetUtil.weeksToAvg());
var alertText = ['Account ' + account.getCustomerId()];
var validWhite = ['', 'white', '#ffffff']; // these all count as white
// Colors cells that need alerting, and adds text to the alert email body.
function generateAlert(field, emailAlertText) {
// There are 2 cells to check, for Today's value and Past value
var bgRange = [
sheet.getRange(startingRow, STATS[field].Column, 1, 1),
sheet.getRange(startingRow, STATS[field].Column + STATS.NumOfColumns,
1, 1)
];
var bg = [bgRange[0].getBackground(), bgRange[1].getBackground()];
// If both backgrounds are white, change background Colors
// and update most recent alert time.
if ((-1 != validWhite.indexOf(bg[0])) &&
(-1 != validWhite.indexOf(bg[1]))) {
bgRange[0].setBackground([[STATS[field]['Color']]]);
bgRange[1].setBackground([[STATS[field]['Color']]]);
spreadsheet.getRangeByName(STATS[field]['AlertRange']).
setValue('Alert at ' + hours + ':00');
alertText.push(emailAlertText);
}
}
if (thresholds.Impressions &&
todayStats.Impressions < pastStats.Impressions * thresholds.Impressions) {
generateAlert('Impressions',
' Impressions are too low: ' + todayStats.Impressions +
' Impressions by ' + hours + ':00, expecting at least ' +
parseInt(pastStats.Impressions * thresholds.Impressions));
}
if (thresholds.Clicks &&
todayStats.Clicks < (pastStats.Clicks * thresholds.Clicks).toFixed(1)) {
generateAlert('Clicks',
' Clicks are too low: ' + todayStats.Clicks +
' Clicks by ' + hours + ':00, expecting at least ' +
(pastStats.Clicks * thresholds.Clicks).toFixed(1));
}
if (thresholds.Conversions &&
todayStats.Conversions <
(pastStats.Conversions * thresholds.Conversions).toFixed(1)) {
generateAlert(
'Conversions',
' Conversions are too low: ' + todayStats.Conversions +
' Conversions by ' + hours + ':00, expecting at least ' +
(pastStats.Conversions * thresholds.Conversions).toFixed(1));
}
if (thresholds.Cost &&
todayStats.Cost > (pastStats.Cost * thresholds.Cost).toFixed(2)) {
generateAlert(
'Cost',
' Cost is too high: ' + todayStats.Cost + ' ' +
account.getCurrencyCode() + ' by ' + hours +
':00, expecting at most ' +
(pastStats.Cost * thresholds.Cost).toFixed(2));
}
// If no alerts were triggered, we will have only the heading text. Remove it.
if (alertText.length == 1) {
alertText = [];
}
var dataRows = [[
account.getCustomerId(), todayStats.Impressions, todayStats.Clicks,
todayStats.Conversions, todayStats.Cost, pastStats.Impressions.toFixed(0),
pastStats.Clicks.toFixed(1), pastStats.Conversions.toFixed(1),
pastStats.Cost.toFixed(2)
]];
sheet.getRange(startingRow, CONST.FIRST_DATA_COLUMN,
1, CONST.TOTAL_DATA_COLUMNS).setValues(dataRows);
return alertText;
}
var SheetUtil = (function() {
var thresholds = {};
var upToHour = 1; // default
var weeks = 26; // default
var todayQuery = '';
var pastQuery = '';
var setupData = function(spreadsheet) {
Logger.log('Running setupData');
spreadsheet.getRangeByName('date').setValue(new Date());
spreadsheet.getRangeByName('account_id').setValue(
mccManager.mccAccount().getCustomerId());
var getThresholdFor = function(field) {
thresholds[field] = parseField(spreadsheet.
getRangeByName(field).getValue());
};
getThresholdFor('Impressions');
getThresholdFor('Clicks');
getThresholdFor('Conversions');
getThresholdFor('Cost');
var now = new Date();
// Basic reporting statistics are usually available with no more than a 3-hour
// delay.
var upTo = new Date(now.getTime() - 3 * 3600 * 1000);
upToHour = parseInt(getDateStringInTimeZone('h', upTo));
spreadsheet.getRangeByName('timestamp').setValue(
DAYS[getDateStringInTimeZone('u', now)] + ', ' + upToHour + ':00');
if (upToHour == 1) {
// First run of the day, clear existing alerts.
spreadsheet.getRangeByName(STATS['Clicks']['AlertRange']).clearContent();
spreadsheet.getRangeByName(STATS['Impressions']['AlertRange']).
clearContent();
spreadsheet.getRangeByName(STATS['Conversions']['AlertRange'])
.clearContent();
spreadsheet.getRangeByName(STATS['Cost']['AlertRange']).clearContent();
// Reset background and font Colors for all data rows.
var bg = [];
var ft = [];
var bg_single = [
'white', 'white', 'white', 'white', 'white', 'white', 'white', 'white',
'white'
];
var ft_single = [
'black', 'black', 'black', 'black', 'black', 'black', 'black', 'black',
'black'
];
// Construct a 50-row array of colors to set.
for (var a = 0; a < CONST.MCC_CHILD_ACCOUNT_LIMIT; ++a) {
bg.push(bg_single);
ft.push(ft_single);
}
var dataRegion = spreadsheet.getSheets()[0].getRange(
CONST.FIRST_DATA_ROW, CONST.FIRST_DATA_COLUMN,
CONST.MCC_CHILD_ACCOUNT_LIMIT, CONST.TOTAL_DATA_COLUMNS);
dataRegion.setBackgrounds(bg);
dataRegion.setFontColors(ft);
}
var weeksStr = spreadsheet.getRangeByName('weeks').getValue();
weeks = parseInt(weeksStr.substring(0, weeksStr.indexOf(' ')));
var dateRangeToCheck = getDateStringInPast(0, upTo);
var dateRangeToEnd = getDateStringInPast(1, upTo);
var dateRangeToStart = getDateStringInPast(1 + weeks * 7, upTo);
var fields = 'HourOfDay, DayOfWeek, Clicks, Impressions, Conversions, Cost';
todayQuery = 'SELECT ' + fields +
' FROM ACCOUNT_PERFORMANCE_REPORT DURING ' + dateRangeToCheck + ',' +
dateRangeToCheck;
pastQuery = 'SELECT ' + fields +
' FROM ACCOUNT_PERFORMANCE_REPORT WHERE DayOfWeek=' +
DAYS[getDateStringInTimeZone('u', now)].toUpperCase() +
' DURING ' + dateRangeToStart + ',' + dateRangeToEnd;
};
var getThresholds = function() { return thresholds; };
var getHourOfDay = function() { return upToHour; };
var getWeeksToAvg = function() { return weeks; };
var getPastQuery = function() { return pastQuery; };
var getTodayQuery = function() { return todayQuery; };
// The SheetUtil public interface.
return {
setupData: setupData,
thresholds: getThresholds,
hourOfDay: getHourOfDay,
weeksToAvg: getWeeksToAvg,
getPastQuery: getPastQuery,
getTodayQuery: getTodayQuery
};
})();
function sendEmail(account, alertTextArray, spreadsheet) {
var bodyText = '';
alertTextArray.forEach(function(alertText) {
// When zero alerts, this is an empty array, which we don't want to add.
if (alertText.length == 0) { return }
bodyText += alertText.join('\n') + '\n\n';
});
bodyText = bodyText.trim();
var email = spreadsheet.getRangeByName('email').getValue();
if (bodyText.length > 0 && email && email.length > 0 &&
email != 'foo#example.com') {
Logger.log('Sending Email');
MailApp.sendEmail(email,
'AdWords Account ' + account.getCustomerId() + ' misbehaved.',
'Your account ' + account.getCustomerId() +
' is not performing as expected today: \n\n' +
bodyText + '\n\n' +
'Log into AdWords and take a look: ' +
'adwords.google.com\n\nAlerts dashboard: ' +
SPREADSHEET_URL);
}
else if (bodyText.length == 0) {
Logger.log('No alerts triggered. No email being sent.');
}
}
function toFloat(value) {
value = value.toString().replace(/,/g, '');
return parseFloat(value);
}
function parseField(value) {
if (value == 'No alert') {
return null;
} else {
return toFloat(value);
}
}
function accumulateRows(rows, hours, weeks) {
var result = {Clicks: 0, Impressions: 0, Conversions: 0, Cost: 0};
while (rows.hasNext()) {
var row = rows.next();
var hour = row['HourOfDay'];
if (hour < hours) {
result = addRow(row, result, 1 / weeks);
}
}
return result;
}
function addRow(row, previous, coefficient) {
if (!coefficient) {
coefficient = 1;
}
if (!row) {
row = {Clicks: 0, Impressions: 0, Conversions: 0, Cost: 0};
}
if (!previous) {
previous = {Clicks: 0, Impressions: 0, Conversions: 0, Cost: 0};
}
return {
Clicks: parseInt(row['Clicks']) * coefficient + previous.Clicks,
Impressions:
parseInt(row['Impressions']) * coefficient + previous.Impressions,
Conversions:
parseInt(row['Conversions']) * coefficient + previous.Conversions,
Cost: toFloat(row['Cost']) * coefficient + previous.Cost
};
}
function checkInRange(today, yesterday, coefficient, field) {
var yesterdayValue = yesterday[field] * coefficient;
if (today[field] > yesterdayValue * 2) {
Logger.log('' + field + ' too much');
} else if (today[field] < yesterdayValue / 2) {
Logger.log('' + field + ' too little');
}
}
/**
* Produces a formatted string representing a date in the past of a given date.
*
* #param {number} numDays The number of days in the past.
* #param {date} date A date object. Defaults to the current date.
* #return {string} A formatted string in the past of the given date.
*/
function getDateStringInPast(numDays, date) {
date = date || new Date();
var MILLIS_PER_DAY = 1000 * 60 * 60 * 24;
var past = new Date(date.getTime() - numDays * MILLIS_PER_DAY);
return getDateStringInTimeZone('yyyyMMdd', past);
}
/**
* Produces a formatted string representing a given date in a given time zone.
*
* #param {string} format A format specifier for the string to be produced.
* #param {date} date A date object. Defaults to the current date.
* #param {string} timeZone A time zone. Defaults to the account's time zone.
* #return {string} A formatted string of the given date in the given time zone.
*/
function getDateStringInTimeZone(format, date, timeZone) {
date = date || new Date();
timeZone = timeZone || AdWordsApp.currentAccount().getTimeZone();
return Utilities.formatDate(date, timeZone, format);
}
/**
* Module that deals with fetching and iterating through multiple accounts.
*
* #return {object} callable functions corresponding to the available
* actions. Specifically, it currently supports next, current, mccAccount.
*/
var mccManager = (function() {
var accountIterator;
var mccAccount;
var currentAccount;
// Private one-time init function.
var init = function() {
var accountSelector = MccApp.accounts();
// Use this to limit the accounts that are being selected in the report.
if (CONFIG.ACCOUNT_LABEL) {
accountSelector.withCondition("LabelNames CONTAINS '" +
CONFIG.ACCOUNT_LABEL + "'");
}
accountSelector.withLimit(CONST.MCC_CHILD_ACCOUNT_LIMIT);
accountIterator = accountSelector.get();
mccAccount = AdWordsApp.currentAccount(); // save the mccAccount
currentAccount = AdWordsApp.currentAccount();
};
/**
* After calling this, AdWordsApp will have the next account selected.
* If there are no more accounts to process, re-selects the original
* MCC account.
*
* #return {AdWordsApp.Account} The account that has been selected.
*/
var getNextAccount = function() {
if (accountIterator.hasNext()) {
currentAccount = accountIterator.next();
MccApp.select(currentAccount);
return currentAccount;
}
else {
MccApp.select(mccAccount);
return null;
}
};
/**
* Returns the currently selected account. This is cached for performance.
*
* #return {AdWords.Account} The currently selected account.
*/
var getCurrentAccount = function() {
return currentAccount;
};
/**
* Returns the original MCC account.
*
* #return {AdWords.Account} The original account that was selected.
*/
var getMccAccount = function() {
return mccAccount;
};
// Set up internal variables; called only once, here.
init();
// Expose the external interface.
return {
next: getNextAccount,
current: getCurrentAccount,
mccAccount: getMccAccount
};
})();
/**
* Validates the provided spreadsheet URL and email address
* to make sure that they're set up properly. Throws a descriptive error message
* if validation fails.
*
* #param {string} spreadsheeturl The URL of the spreadsheet to open.
* #return {Spreadsheet} The spreadsheet object itself, fetched from the URL.
* #throws {Error} If the spreadsheet URL or email hasn't been set
*/
function validateAndGetSpreadsheet(spreadsheeturl) {
if (spreadsheeturl == 'YOUR_SPREADSHEET_URL') {
throw new Error('Please specify a valid Spreadsheet URL. You can find' +
' a link to a template in the associated guide for this script.');
}
var spreadsheet = SpreadsheetApp.openByUrl(spreadsheeturl);
var email = spreadsheet.getRangeByName('email').getValue();
if ('foo#example.com' == email) {
throw new Error('Please either set a custom email address in the' +
' spreadsheet, or set the email field in the spreadsheet to blank' +
' to send no email.');
}
return spreadsheet;
}
im rearranging a spreadsheet using google script. I am first sorting it and then moving some rows within a range to the end of the sheet. Everytime i do the moveTo function some cells that reference the moved rows get changed to reflect the new row numbers even though the cells are outside my range and should not be modified. For example if im moving cell b3 and i have cell f4 =b3 then when i move b3 cell f4 changes to be whatever b3 is now. i tried locking it with =b$3 but still didnt work. Also it messes up the conditional formatting that should be in place for the entire column using something like "d2:e" and it changes to be something like "d2:e109" or something similar. Any clue whats going on?
function onEdit(){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var allowedSheet = 1;
if(sheet.getIndex() == allowedSheet) {
var editedCell = sheet.getActiveCell();
var sortBy = [1, 3, 2];
var triggerCol = [1,2,3,10,11,12];
var rangeStart = "A";
var rangeEnd = "E";
var tableRange = "A2:E";
if(triggerCol.indexOf(editedCell.getColumn()) > -1) {
var range = sheet.getRange(tableRange);
range.sort([{column: sortBy[0], ascending: true}, {column: sortBy[1], ascending: false}, {column: sortBy[2], ascending: true}]);
var indexOfIncome = find( sheet, 2, "Income");
if( indexOfIncome > 0 ){
var overflowRange = sheet.getRange("A2:E" + (indexOfIncome - 1 ));
var lastRow = findFirstEmptyCell( sheet, 1 );
overflowRange.moveTo(sheet.getRange("A" + ( lastRow )));
var fullRange = sheet.getRange(rangeStart + indexOfIncome + ":" + rangeEnd);
fullRange.moveTo(sheet.getRange(rangeStart + "2"));
}
}
}
}
function find( sheet, column, value ) {
var data = sheet.getRange(1, column, sheet.getMaxRows()).getValues();
for( i = 0; i < sheet.getMaxRows(); i++ ){
if (data[i][0].toString().indexOf(value) > -1 ) {
return i + 1;
}
}
return 0;
}
function findFirstEmptyCell ( sheet, column ){
var data = sheet.getRange( 1, column, sheet.getMaxRows() ).getValues();
for( i = 0; i < sheet.getMaxRows() ; i++ ){
if( data[i][0] == "" ){
return i + 1;
}
}
return 0;
}
It's an expected behaviour, moveTo works just like Cut (Ctrl+X), while your looking for Copy+Paste then delete original content, which should be copyTo(newRange) associated with clear(oldRange).
I got it to work by using copy and then clearing. Now why in the world google would make a single function that behaves the exact opposite from all the other functions is beyond me. The documentation says moveto behaves as cut and paste but when i cut and paste i do not mess up the rest of my sheet.