Đặt giá thầu theo thời tiết

Biểu tượng đặt giá thầu

Nhu cầu đối với một số sản phẩm và dịch vụ thay đổi đáng kể tuỳ thuộc vào thời tiết. Ví dụ: người dùng có nhiều khả năng tìm kiếm thông tin về công viên giải trí vào một ngày nắng nóng hơn nhiều so với khi trời lạnh và mưa. Một công ty công viên giải trí có thể muốn tăng giá thầu khi thời tiết đẹp, nhưng việc này sẽ đòi hỏi nhiều công việc thủ công mỗi ngày. Tuy nhiên, với tập lệnh Google Ads, bạn vẫn có thể tìm nạp thông tin thời tiết theo phương thức lập trình và điều chỉnh giá thầu chỉ trong vài phút.

Tập lệnh này sử dụng Bảng tính Google để lưu trữ danh sách các chiến dịch và vị trí được liên kết của các chiến dịch. Lệnh gọi OpenWeatherMap được thực hiện cho từng vị trí và điều kiện thời tiết được tính toán bằng một số quy tắc cơ bản. Nếu quy tắc được đánh giá là true, thì hệ số giá thầu vị trí tương ứng sẽ được áp dụng cho tính năng nhắm mục tiêu theo vị trí cho chiến dịch.

Cách thức hoạt động

Tập lệnh hoạt động bằng cách đọc dữ liệu trên bảng tính. Bảng tính bao gồm ba trang tính riêng lẻ:

1. Dữ liệu về chiến dịch

Một bộ quy tắc xác định hệ số sửa đổi giá thầu sẽ được áp dụng cho chiến dịch khi đáp ứng một điều kiện thời tiết. Sau đây là các cột bắt buộc:

  • Tên chiến dịch: Tên của chiến dịch cần sửa đổi.
  • Thời tiết: Vị trí để kiểm tra điều kiện thời tiết.
  • Điều kiện thời tiết: Điều kiện thời tiết mà quy tắc này được áp dụng.
  • Hệ số điều chỉnh giá thầu: Hệ số điều chỉnh giá thầu vị trí sẽ được áp dụng nếu đáp ứng điều kiện thời tiết.
  • Áp dụng đối tượng sửa đổi cho: Liệu hệ số sửa đổi giá thầu chỉ được áp dụng cho các mục tiêu theo địa lý của chiến dịch khớp với Vị trí thời tiết hay cho tất cả các mục tiêu theo vị trí địa lý của chiến dịch.
  • Enabled (Bật): Chỉ định Yes để bật một quy tắc và No để tắt quy tắc đó.

Ví dụ:

Ví dụ sau đây có 3 chiến dịch.

Ảnh chụp màn hình bảng tính, trang tính 1

Chiến dịch thử nghiệm 1 minh hoạ một tình huống sử dụng điển hình. Chiến dịch này đang nhắm đến Boston, MA và có 2 quy tắc:

  1. Áp dụng hệ số điều chỉnh giá thầu là 1.3 nếu thời tiết ở Boston, MA là Sunny.
  2. Áp dụng hệ số điều chỉnh giá thầu là 0.8 nếu thời tiết ở Boston, MA là Rainy.

Chiến dịch thử nghiệm 2 có quy tắc đặt giá thầu giống như Chiến dịch thử nghiệm 1, nhưng nhắm mục tiêu đến Connecticut.

Chiến dịch thử nghiệm 3 cũng sử dụng cùng quy tắc đặt giá thầu nhưng nhắm mục tiêu đến Florida. Vì các quy tắc thời tiết của Florida được liên kết với toàn bộ tiểu bang, đây không phải là vị trí mà chiến dịch nhắm mục tiêu rõ ràng, nên "Apply Modifier To" (Áp dụng đối tượng sửa đổi) được đặt thành All Geo Targets để các thành phố mà chiến dịch nhắm đến sẽ bị ảnh hưởng.

2. Dữ liệu thời tiết

Trang tính này xác định điều kiện thời tiết được dùng trong bảng dữ liệu Chiến dịch. Sau đây là những cột bắt buộc:

  • Condition Name (Tên điều kiện): Tên điều kiện thời tiết (ví dụ: Sunny).
  • Nhiệt độ: Nhiệt độ theo độ F.
  • Lượng mưa: Lượng mưa (tính bằng milimét) trong 3 giờ qua.
  • Vận tốc gió: Tốc độ gió, tính bằng dặm/giờ.

Ảnh chụp màn hình bảng tính, trang tính 2

Trang tính ở trên xác định hai điều kiện thời tiết:

  1. Sunny: Nhiệt độ nằm trong khoảng từ 65 đến 80 độ F, lượng mưa dưới 1 mm trong 3 giờ qua và tốc độ gió dưới 5 dặm/giờ.
  2. Rainy: Lượng mưa trên 0 mm trong 3 giờ qua và tốc độ gió dưới 10 dặm/giờ.

Điều kiện thời tiết

Khi xác định điều kiện thời tiết, hãy chỉ định các giá trị như sau:

  • below x: Giá trị được chỉ định là below x (ví dụ: below 10)
  • above x: Giá trị được chỉ định là above x (ví dụ: above 70)
  • x to y: Giá trị được chỉ định nằm trong khoảng từ x đến y, tính cả 2 giá trị đầu cuối (ví dụ: 65 to 80)

Nếu bạn để trống một ô, thì tham số đó không được xem xét trong các phép tính. Vì vậy, trong ví dụ của chúng ta, vì điều kiện thời tiết Rainy có cột nhiệt độ trống, nên nhiệt độ sẽ không được xem xét khi tính toán điều kiện thời tiết này.

Điều kiện thời tiết được kết hợp với nhau khi tính toán điều kiện thời tiết. Trong ví dụ này, điều kiện thời tiết Sunny được đánh giá như sau:

const isSunny = (temperature >= 65 && temperature <= 80) && (precipitation < 1) && (wind < 5);

3. Dữ liệu vị trí thời tiết

Trang tính này xác định các vị trí thời tiết được dùng trong bảng dữ liệu Chiến dịch và bao gồm hai cột:

  • Thời tiết: Đây là tên vị trí thời tiết, theo cách hiểu của API OpenWeatherMap.
  • Mã mục tiêu địa lý: Đây là mã mục tiêu địa lý, theo cách hiểu của Google Ads.

Tập lệnh cho phép bạn chỉ định nhiều mã mục tiêu địa lý cho một vị trí thời tiết duy nhất, vì các vị trí thời tiết không phải lúc nào cũng chi tiết như các tuỳ chọn nhắm mục tiêu có trong Google Ads. Bạn có thể thực hiện việc liên kết một vị trí thời tiết duy nhất đến nhiều vị trí địa lý bằng cách có nhiều hàng có cùng vị trí thời tiết nhưng mỗi hàng có mã địa lý khác nhau.

Ảnh chụp màn hình bảng tính, trang tính 3

Đối với ví dụ này, có 3 vị trí thời tiết được xác định:

  1. Boston, MA: Mã địa lý 10108127
  2. Connecticut: Mã địa lý 1014778, 10147431014843, tương ứng với ba thành phố ở Connecticut
  3. Florida: Mã địa lý 21142

Mục tiêu vùng lân cận

Bạn có thể áp dụng các quy tắc chiến dịch sử dụng Matching Geo Targets cho các vị trí được nhắm mục tiêu, vùng lân cận được nhắm mục tiêu hoặc cả hai bằng cách sử dụng cờ TARGETING.

Tính năng nhắm mục tiêu theo vị trí so khớp mã địa lý với mã vị trí.

Tính năng nhắm mục tiêu vùng lân cận xác minh rằng toạ độ vĩ độ và kinh độ được chỉ định nằm trong bán kính vùng lân cận bằng công thức Haversine.

Logic tập lệnh

Tập lệnh bắt đầu bằng cách đọc quy tắc từ cả ba trang tính. Sau đó, trình phân tích cú pháp sẽ cố gắng thực thi từng quy tắc từ Trang tính chiến dịch theo trình tự.

Đối với mỗi quy tắc được thực thi, tập lệnh sẽ kiểm tra xem chiến dịch có nhắm mục tiêu vị trí đã chỉ định hay không. Nếu có thì tập lệnh sẽ truy xuất hệ số sửa đổi giá thầu hiện tại.

Tiếp theo, bạn có thể truy xuất điều kiện thời tiết của vị trí đó bằng cách gọi API OpenWeatherMap. Sau đó, các quy tắc về điều kiện thời tiết sẽ được đánh giá để xem điều kiện thời tiết của địa điểm có khớp với thông tin được chỉ định trong quy tắc hay không. Nếu có và hệ số sửa đổi giá thầu mới khác với công cụ sửa đổi giá thầu hiện tại, thì tập lệnh sẽ sửa đổi hệ số sửa đổi giá thầu cho vị trí đó.

Sẽ không có thay đổi nào nếu điều kiện thời tiết không khớp, nếu các giá trị của hệ số sửa đổi giá thầu giống nhau hoặc nếu Áp dụng đối tượng sửa đổi cho của quy tắc là Matching Geo Targets, nhưng chiến dịch không nhắm đến những vị trí được liên kết với quy tắc đó.

Thiết lập

  • Đăng ký khoá API tại openweathermap.org.
  • Tạo bản sao của bảng tính mẫu rồi chỉnh sửa chiến dịch và các quy tắc thời tiết.
  • Tạo tập lệnh mới bằng mã nguồn bên dưới.
  • Cập nhật các biến OPEN_WEATHER_MAP_API_KEY, SPREADSHEET_URLTARGETING trong tập lệnh.
  • Lên lịch để chạy theo yêu cầu.

Mã nguồn

// Copyright 2015, 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 Bid By Weather
 *
 * @overview The Bid By Weather script adjusts campaign bids by weather
 *     conditions of their associated locations. See
 *     https://developers.google.com/google-ads/scripts/docs/solutions/weather-based-campaign-management#bid-by-weather
 *     for more details.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 2.0
 *
 * @changelog
 * - version 2.0
 *   - Updated to use new Google Ads Scripts features.
 * - version 1.2.2
 *   - Add support for video and shopping campaigns.
 * - version 1.2.1
 *   - Added validation for external spreadsheet setup.
 * - version 1.2
 *   - Added proximity based targeting.  Targeting flag allows location
 *     targeting, proximity targeting or both.
 * - version 1.1
 *   - Added flag allowing bid adjustments on all locations targeted by
 *     a campaign rather than only those that match the campaign rule
 * - version 1.0
 *   - Released initial version.
 */

// Register for an API key at http://openweathermap.org/appid
// and enter the key below.
const OPEN_WEATHER_MAP_API_KEY = 'INSERT_OPEN_WEATHER_MAP_API_KEY_HERE';

// Create a copy of https://goo.gl/A59Uuc and enter the URL below.
const SPREADSHEET_URL = 'INSERT_SPREADSHEET_URL_HERE';

// A cache to store the weather for locations already lookedup earlier.
const WEATHER_LOOKUP_CACHE = {};

// Flag to pick which kind of targeting "LOCATION", "PROXIMITY", or "ALL".
const TARGETING = 'ALL';

/**
 * According to the list of campaigns and their associated locations, the script
 * makes a call to the OpenWeatherMap API for each location.
 * Based on the weather conditions, the bids are adjusted.
 */
function main() {
  validateApiKey();
  // Load data from spreadsheet.
  const spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
  const campaignRuleData = getSheetData(spreadsheet, 1);
  const weatherConditionData = getSheetData(spreadsheet, 2);
  const geoMappingData = getSheetData(spreadsheet, 3);

  // Convert the data into dictionaries for convenient usage.
  const campaignMapping = buildCampaignRulesMapping(campaignRuleData);
  const weatherConditionMapping =
      buildWeatherConditionMapping(weatherConditionData);
  const locationMapping = buildLocationMapping(geoMappingData);

  // Apply the rules.
  for (const campaignName in campaignMapping) {
    applyRulesForCampaign(campaignName, campaignMapping[campaignName],
        locationMapping, weatherConditionMapping);
  }
}

/**
 * Retrieves the data for a worksheet.
 *
 * @param {Object} spreadsheet The spreadsheet.
 * @param {number} sheetIndex The sheet index.
 * @return {Array} The data as a two dimensional array.
 */
function getSheetData(spreadsheet, sheetIndex) {
  const sheet = spreadsheet.getSheets()[sheetIndex];
  const range =
      sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn());
  return range.getValues();
}

/**
 * Builds a mapping between the list of campaigns and the rules
 * being applied to them.
 *
 * @param {Array} campaignRulesData The campaign rules data, from the
 *     spreadsheet.
 * @return {!Object.<string, Array.<Object>> } A map, with key as campaign name,
 *     and value as an array of rules that apply to this campaign.
 */
function buildCampaignRulesMapping(campaignRulesData) {
  const campaignMapping = {};
  for (const rules of campaignRulesData) {
    // Skip rule if not enabled.

    if (rules[5].toLowerCase() == 'yes') {
      const campaignName = rules[0];
      const campaignRules = campaignMapping[campaignName] || [];
      campaignRules.push({
          'name': campaignName,

          // location for which this rule applies.
          'location': rules[1],

          // the weather condition (e.g. Sunny).
          'condition': rules[2],

          // bid modifier to be applied.
          'bidModifier': rules[3],

          // whether bid adjustments should by applied only to geo codes
          // matching the location of the rule or to all geo codes that
          // the campaign targets.
          'targetedOnly': rules[4].toLowerCase() ==
                          'matching geo targets'
      });
      campaignMapping[campaignName] = campaignRules;
    }
  }
  Logger.log('Campaign Mapping: %s', campaignMapping);
  return campaignMapping;
}

/**
 * Builds a mapping between a weather condition name (e.g. Sunny) and the rules
 * that correspond to that weather condition.
 *
 * @param {Array} weatherConditionData The weather condition data from the
 *      spreadsheet.
 * @return {!Object.<string, Array.<Object>>} A map, with key as a weather
 *     condition name, and value as the set of rules corresponding to that
 *     weather condition.
 */
function buildWeatherConditionMapping(weatherConditionData) {
  const weatherConditionMapping = {};
  for (const weatherCondition of weatherConditionData) {
    const weatherConditionName = weatherCondition[0];
    weatherConditionMapping[weatherConditionName] = {
      // Condition name (e.g. Sunny)
      'condition': weatherConditionName,

      // Temperature (e.g. 50 to 70)
      'temperature': weatherCondition[1],

      // Precipitation (e.g. below 70)
      'precipitation': weatherCondition[2],

      // Wind speed (e.g. above 5)
      'wind': weatherCondition[3]
    };
  }
  Logger.log('Weather condition mapping: %s', weatherConditionMapping);
  return weatherConditionMapping;
}

/**
 * Builds a mapping between a location name (as understood by OpenWeatherMap
 * API) and a list of geo codes as identified by Google Ads scripts.
 *
 * @param {Array} geoTargetData The geo target data from the spreadsheet.
 * @return {!Object.<string, Array.<Object>>} A map, with key as a locaton name,
 *     and value as an array of geo codes that correspond to that location
 *     name.
 */
function buildLocationMapping(geoTargetData) {
  const locationMapping = {};
  for (const geoTarget of geoTargetData) {
    const locationName = geoTarget[0];
    const locationDetails = locationMapping[locationName] || {
      'geoCodes': []      // List of geo codes understood by Google Ads scripts.
    };
    locationDetails.geoCodes.push(geoTarget[1]);
    locationMapping[locationName] = locationDetails;
  }
  Logger.log('Location Mapping: %s', locationMapping);
  return locationMapping;
}

/**
 * Applies rules to a campaign.
 *
 * @param {string} campaignName The name of the campaign.
 * @param {Object} campaignRules The details of the campaign. See
 *     buildCampaignMapping for details.
 * @param {Object} locationMapping Mapping between a location name (as
 *     understood by OpenWeatherMap API) and a list of geo codes as
 *     identified by Google Ads scripts. See buildLocationMapping for details.
 * @param {Object} weatherConditionMapping Mapping between a weather condition
 *     name (e.g. Sunny) and the rules that correspond to that weather
 *     condition. See buildWeatherConditionMapping for details.
 */
function applyRulesForCampaign(campaignName, campaignRules, locationMapping,
                               weatherConditionMapping) {
  for (const rules of campaignRules) {
    let bidModifier = 1;
    const campaignRule = rules;

    // Get the weather for the required location.
    const locationDetails = locationMapping[campaignRule.location];
    const weather = getWeather(campaignRule.location);
    Logger.log('Weather for %s: %s', locationDetails, weather);

    // Get the weather rules to be checked.
    const weatherConditionName = campaignRule.condition;
    const weatherConditionRules = weatherConditionMapping[weatherConditionName];

    // Evaluate the weather rules.
    if (evaluateWeatherRules(weatherConditionRules, weather)) {
      Logger.log('Matching Rule found: Campaign Name = %s, location = %s, ' +
          'weatherName = %s,weatherRules = %s, noticed weather = %s.',
          campaignRule.name, campaignRule.location,
          weatherConditionName, weatherConditionRules, weather);
      bidModifier = campaignRule.bidModifier;

      if (TARGETING == 'LOCATION' || TARGETING == 'ALL') {
        // Get the geo codes that should have their bids adjusted.
        const geoCodes = campaignRule.targetedOnly ?
          locationDetails.geoCodes : null;
        adjustBids(campaignName, geoCodes, bidModifier);
      }

      if (TARGETING == 'PROXIMITY' || TARGETING == 'ALL') {
        const location = campaignRule.targetedOnly ? campaignRule.location : null;
        adjustProximityBids(campaignName, location, bidModifier);
      }
    }
  }
  return;
}

/**
 * Converts a temperature value from kelvin to fahrenheit.
 *
 * @param {number} kelvin The temperature in Kelvin scale.
 * @return {number} The temperature in Fahrenheit scale.
 */
function toFahrenheit(kelvin) {
  return (kelvin - 273.15) * 1.8 + 32;
}

/**
 * Evaluates the weather rules.
 *
 * @param {Object} weatherRules The weather rules to be evaluated.
 * @param {Object.<string, string>} weather The actual weather.
 * @return {boolean} True if the rule matches current weather conditions,
 *     False otherwise.
 */
function evaluateWeatherRules(weatherRules, weather) {
  // See https://openweathermap.org/weather-data
  // for values returned by OpenWeatherMap API.
  let precipitation = 0;
  if (weather.rain && weather.rain['3h']) {
    precipitation = weather.rain['3h'];
  }
  const temperature = toFahrenheit(weather.main.temp);
  const windspeed = weather.wind.speed;

  return evaluateMatchRules(weatherRules.temperature, temperature) &&
      evaluateMatchRules(weatherRules.precipitation, precipitation) &&
      evaluateMatchRules(weatherRules.wind, windspeed);
}

/**
 * Evaluates a condition for a value against a set of known evaluation rules.
 *
 * @param {string} condition The condition to be checked.
 * @param {Object} value The value to be checked.
 * @return {boolean} True if an evaluation rule matches, false otherwise.
 */
function evaluateMatchRules(condition, value) {
  // No condition to evaluate, rule passes.
  if (condition == '') {
    return true;
  }
  const rules = [matchesBelow, matchesAbove, matchesRange];

  for (const rule of rules) {
    if (rule(condition, value)) {
      return true;
    }
  }
  return false;
}

/**
 * Evaluates whether a value is below a threshold value.
 *
 * @param {string} condition The condition to be checked. (e.g. below 50).
 * @param {number} value The value to be checked.
 * @return {boolean} True if the value is less than what is specified in
 * condition, false otherwise.
 */
function matchesBelow(condition, value) {
  conditionParts = condition.split(' ');

  if (conditionParts.length != 2) {
    return false;
  }

  if (conditionParts[0] != 'below') {
    return false;
  }

  if (value < conditionParts[1]) {
    return true;
  }
  return false;
}

/**
 * Evaluates whether a value is above a threshold value.
 *
 * @param {string} condition The condition to be checked. (e.g. above 50).
 * @param {number} value The value to be checked.
 * @return {boolean} True if the value is greater than what is specified in
 *     condition, false otherwise.
 */
function matchesAbove(condition, value) {
  conditionParts = condition.split(' ');

  if (conditionParts.length != 2) {
    return false;
  }

  if (conditionParts[0] != 'above') {
    return false;
  }

  if (value > conditionParts[1]) {
    return true;
  }
  return false;
}

/**
 * Evaluates whether a value is within a range of values.
 *
 * @param {string} condition The condition to be checked (e.g. 5 to 18).
 * @param {number} value The value to be checked.
 * @return {boolean} True if the value is in the desired range, false otherwise.
 */
function matchesRange(condition, value) {
  conditionParts = condition.replace('w+', ' ').split(' ');

  if (conditionParts.length != 3) {
    return false;
  }

  if (conditionParts[1] != 'to') {
    return false;
  }

  if (conditionParts[0] <= value && value <= conditionParts[2]) {
    return true;
  }
  return false;
}

/**
 * Retrieves the weather for a given location, using the OpenWeatherMap API.
 *
 * @param {string} location The location to get the weather for.
 * @return {Object.<string, string>} The weather attributes and values, as
 *     defined in the API.
 */
function getWeather(location) {
  if (location in WEATHER_LOOKUP_CACHE) {
    Logger.log('Cache hit...');
    return WEATHER_LOOKUP_CACHE[location];
  }
  const url=`http://api.openweathermap.org/data/2.5/weather?APPID=${OPEN_WEATHER_MAP_API_KEY}&q=${location}`;
  const response = UrlFetchApp.fetch(url);
  if (response.getResponseCode() != 200) {
    throw Utilities.formatString(
        'Error returned by API: %s, Location searched: %s.',
        response.getContentText(), location);
  }
  const result = JSON.parse(response.getContentText());

  // OpenWeatherMap's way of returning errors.
  if (result.cod != 200) {
    throw Utilities.formatString(
        'Error returned by API: %s,  Location searched: %s.',
        response.getContentText(), location);
  }
  WEATHER_LOOKUP_CACHE[location] = result;
  return result;
}

/**
 * Adjusts the bidModifier for a list of geo codes for a campaign.
 *
 * @param {string} campaignName The name of the campaign.
 * @param {Array} geoCodes The list of geo codes for which bids should be
 *     adjusted.  If null, all geo codes on the campaign are adjusted.
 * @param {number} bidModifier The bid modifier to use.
 */
function adjustBids(campaignName, geoCodes, bidModifier) {
  // Get the campaign.
  const campaign = getCampaign(campaignName);
  if (!campaign) return null;

  // Get the targeted locations.
  const locations = campaign.targeting().targetedLocations().get();
  for (const location of locations) {
    const currentBidModifier = location.getBidModifier().toFixed(2);

    // Apply the bid modifier only if the campaign has a custom targeting
    // for this geo location or if all locations are to be modified.
    if (!geoCodes || (geoCodes.indexOf(location.getId()) != -1 &&
      currentBidModifier != bidModifier)) {
        Logger.log('Setting bidModifier = %s for campaign name = %s, ' +
            'geoCode = %s. Old bid modifier is %s.', bidModifier,
            campaignName, location.getId(), currentBidModifier);
        location.setBidModifier(bidModifier);
    }
  }
}

/**
 * Adjusts the bidModifier for campaigns targeting by proximity location
 * for a given weather location.
 *
 * @param {string} campaignName The name of the campaign.
 * @param {string} weatherLocation The weather location for which bids should be
 *     adjusted.  If null, all proximity locations on the campaign are adjusted.
 * @param {number} bidModifier The bid modifier to use.
 */
function adjustProximityBids(campaignName, weatherLocation, bidModifier) {
  // Get the campaign.
  const campaign = getCampaign(campaignName);
  if(campaign === null) return;

  // Get the proximity locations.
  const proximities = campaign.targeting().targetedProximities().get();
  for (const proximity of proximities) {
    const currentBidModifier = proximity.getBidModifier().toFixed(2);

    // Apply the bid modifier only if the campaign has a custom targeting
    // for this geo location or if all locations are to be modified.
    if (!weatherLocation ||
        (weatherNearProximity(proximity, weatherLocation) &&
           currentBidModifier != bidModifier)) {
        Logger.log('Setting bidModifier = %s for campaign name = %s, with ' +
            'weatherLocation = %s in proximity area. Old bid modifier is %s.',
            bidModifier, campaignName, weatherLocation, currentBidModifier);
        proximity.setBidModifier(bidModifier);
    }
  }
}

/**
 * Checks if weather location is within the radius of the proximity location.
 *
 * @param {Object} proximity The targeted proximity of campaign.
 * @param {string} weatherLocation Name of weather location to check within
 * radius.
 * @return {boolean} Returns true if weather location is within radius.
 */
function weatherNearProximity(proximity, weatherLocation) {
  // See https://en.wikipedia.org/wiki/Haversine_formula for details on how
  // to compute spherical distance.
  const earthRadiusInMiles = 3960.0;
  const degreesToRadians = Math.PI / 180.0;
  const radiansToDegrees = 180.0 / Math.PI;
  const kmToMiles = 0.621371;

  const radiusInMiles = proximity.getRadiusUnits() == 'MILES' ?
    proximity.getRadius() : proximity.getRadius() * kmToMiles;

  // Compute the change in latitude degrees for the radius.
  const deltaLat = (radiusInMiles / earthRadiusInMiles) * radiansToDegrees;
  // Find the radius of a circle around the earth at given latitude.
  const r = earthRadiusInMiles * Math.cos(proximity.getLatitude() *
      degreesToRadians);
  // Compute the change in longitude degrees for the radius.
  const deltaLon = (radiusInMiles / r) * radiansToDegrees;

  // Retrieve weather location for lat/lon coordinates.
  const weather = getWeather(weatherLocation);
  // Check if weather condition is within the proximity boundaries.
  return (weather.coord.lat >= proximity.getLatitude() - deltaLat &&
          weather.coord.lat <= proximity.getLatitude() + deltaLat &&
          weather.coord.lon >= proximity.getLongitude() - deltaLon &&
          weather.coord.lon <= proximity.getLongitude() + deltaLon);
}

/**
 * Finds a campaign by name, whether it is a regular, video, or shopping
 * campaign, by trying all in sequence until it finds one.
 *
 * @param {string} campaignName The campaign name to find.
 * @return {Object} The campaign found, or null if none was found.
 */
function getCampaign(campaignName) {
  const selectors = [AdsApp.campaigns(), AdsApp.videoCampaigns(),
      AdsApp.shoppingCampaigns()];
  for (const selector of selectors) {
    const campaignIter = selector.
        withCondition(`CampaignName = "${campaignName}"`).
        get();
    if (campaignIter.hasNext()) {
      return campaignIter.next();
    }
  }
  return null;
}

/**
 * DO NOT EDIT ANYTHING BELOW THIS LINE.
 * Please modify your spreadsheet URL and API key at the top of the file only.
 */

/**
 * Validates the provided spreadsheet URL to make sure that it's 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 hasn't been set
 */
function validateAndGetSpreadsheet(spreadsheeturl) {
  if (spreadsheeturl == 'INSERT_SPREADSHEET_URL_HERE') {
    throw new Error('Please specify a valid Spreadsheet URL. You can find' +
        ' a link to a template in the associated guide for this script.');
  }
  const spreadsheet = SpreadsheetApp.openByUrl(spreadsheeturl);
  return spreadsheet;
}

/**
 * Validates the provided API key to make sure that it's not the default. Throws
 * a descriptive error message if validation fails.
 *
 * @throws {Error} If the configured API key hasn't been set.
 */
function validateApiKey() {
  if (OPEN_WEATHER_MAP_API_KEY == 'INSERT_OPEN_WEATHER_MAP_API_KEY_HERE') {
    throw new Error('Please specify a valid API key for OpenWeatherMap. You ' +
        'can acquire one here: http://openweathermap.org/appid');
  }
}