ضبط عروض الأسعار حسب الطقس

رمز عروض الأسعار

يختلف الطلب على منتجات وخدمات معينة اختلافًا كبيرًا حسب الطقس. على سبيل المثال، من المرجح أن يبحث المستخدمون عن معلومات عن مدن الملاهي في يوم حار ومشمس أكثر من البحث إذا كان الجو باردًا أو ممطرًا. قد ترغب إحدى شركات مدن الملاهي في زيادة عروض أسعارها عندما يكون الطقس جيدًا، ولكن القيام بذلك كل يوم سيتطلب الكثير من العمل اليدوي. ومع ذلك، باستخدام النصوص البرمجية في إعلانات Google، يمكن جلب معلومات الطقس آليًا وتعديل عروض الأسعار في غضون دقائق.

يستخدم هذا النص البرمجي جداول بيانات Google لتخزين قائمة الحملات والمواقع المرتبطة بها. يتم طلب واجهة برمجة التطبيقات OpenWeatherMap لكل موقع ويتم احتساب ظروف الطقس باستخدام بعض القواعد الأساسية. إذا تم تقييم القاعدة إلى "صحيح"، يتم تطبيق مُضاعِف عروض أسعار الموقع المقابل على الموقع الجغرافي المستهدف للحملة.

آلية العمل

يعمل النص عن طريق قراءة البيانات من جدول بيانات. يتكون جدول البيانات من ثلاث أوراق فردية:

1- بيانات الحملة

تحدّد مجموعة من القواعد معدِّلات عروض الأسعار التي سيتم تطبيقها على الحملات عند استيفاء أحد ظروف الطقس. وفي ما يلي الأعمدة المطلوبة:

  • اسم الحملة: اسم الحملة المطلوب تعديلها.
  • موقع الطقس: الموقع الجغرافي المراد التحقق من أحوال الطقس فيه.
  • حالة الطقس: حالة الطقس التي سيتم تطبيق هذه القاعدة عليها.
  • معدِّل عروض الأسعار: معدِّل عروض الأسعار حسب الموقع الجغرافي الذي سيتم تطبيقه في حال استيفاء حالة الطقس.
  • تطبيق التعديل على: تحديد ما إذا كان سيتم تطبيق معدِّل عروض الأسعار على الاستهدافات الجغرافية للحملة فقط والتي تتطابق مع موقع الطقس أو جميع الاستهدافات الجغرافية للحملة
  • مفعّلة: حدِّد Yes لتفعيل قاعدة وNo لإيقافها.

مثال

يحتوي المثال التالي على ثلاث حملات.

لقطة شاشة لجدول البيانات، الورقة الأولى

توضح الحملة التجريبية 1 سيناريو استخدام نموذجي. تستهدِف الحملة بوسطن، ماساتشوستس ولديها قاعدتَين:

  1. طبِّق أداة تعديل عروض الأسعار بقيمة 1.3 إذا كانت حالة الطقس في بوسطن بولاية ماساتشوستس هي Sunny.
  2. طبِّق أداة تعديل عروض الأسعار بقيمة 0.8 إذا كانت حالة الطقس في بوسطن بولاية ماساتشوستس هي Rainy.

تحتوي الحملة التجريبية 2 على قواعد عروض الأسعار نفسها المتضمّنة في الحملة التجريبية 1، ولكنها تستهدف كونيتيكت.

تستخدِم الحملة الاختبارية 3 أيضًا قواعد عروض الأسعار نفسها، ولكنّها تستهدف فلوريدا. بما أنّه تم ربط قواعد الطقس في فلوريدا بالولاية بأكملها، وهو ليس موقعًا جغرافيًا تستهدفه الحملة بوضوح، تم ضبط "تطبيق التعديل على" على All Geo Targets بحيث تتأثر المدن التي تستهدفها الحملة.

2. بيانات الطقس

تحدّد هذه الورقة أحوال الطقس المستخدمة في ورقة بيانات الحملة. الأعمدة التالية إلزامية:

  • اسم الحالة: اسم حالة الطقس (مثل Sunny).
  • درجة الحرارة: درجة الحرارة بالفهرنهايت.
  • هطول الأمطار: تساقط الأمطار بالمليمتر خلال آخر 3 ساعات.
  • الرياح: سرعة الرياح بالميل في الساعة.

لقطة شاشة لجدول البيانات، الورقة الثانية

تحدد الورقة المعروضة أعلاه حالتين للطقس:

  1. Sunny: تتراوح درجة الحرارة بين 65 و80 درجة فهرنهايت، وهطول الأمطار أقل من 1 ملم خلال آخر 3 ساعات، والرياح أقل من 5 ميل في الساعة.
  2. Rainy: تزداد هطول الأمطار خلال آخر 3 ساعات وتبلغ سرعة الرياح أقل من 10 ميل في الساعة.

أحوال الطقس

عند تحديد أحوال الطقس، حدِّد القيم على النحو التالي:

  • below x: القيمة المحددة هي below x (على سبيل المثال، below 10)
  • above x: القيمة المحددة هي above x (على سبيل المثال، above 70)
  • x to y: تتراوح القيمة المحددة بين x وy، بشكل شامل (على سبيل المثال، 65 to 80)

وإذا تركت خلية فارغة، فلن يتم أخذ هذه المعلمة في الاعتبار في العمليات الحسابية. لذلك في المثال الذي نقدّمه، بما أنّ حالة الطقس في Rainy تحتوي على عمود فارغ لدرجة الحرارة، لن يتم أخذ درجة الحرارة في الاعتبار عند احتساب حالة الطقس هذه.

يتم الجمع بين أحوال الطقس "و" عند حساب حالة الطقس. في هذا المثال، يتم تقييم حالة الطقس في Sunny على النحو التالي:

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

3. بيانات الموقع الجغرافي للطقس

تحدد هذه الورقة مواقع الطقس المستخدمة في ورقة بيانات الحملة، وتتكون من عمودين:

  • موقع الطقس: هو اسم لموقع الطقس، كما هو مفهوم من خلال واجهة برمجة التطبيقات OpenWeatherMap.
  • رمز الاستهداف الجغرافي: هو رمز استهداف جغرافي، ومفهومه "إعلانات Google".

يسمح لك النص البرمجي بتحديد عدة رموز استهداف جغرافي لموقع طقس واحد لأنّ مواقع الطقس لا تكون دائمًا دقيقة مثل خيارات الاستهداف المتاحة في "إعلانات Google". يمكن إجراء تعيين موقع طقس واحد إلى مواقع جغرافية متعددة من خلال وجود صفوف متعددة بنفس موقع الطقس ولكن برموز جغرافية مختلفة لكل صف.

لقطة شاشة لجدول البيانات، الورقة الثالثة

في هذا المثال، هناك ثلاثة مواقع طقس محددة:

  1. Boston, MA: الرمز الجغرافي 10108127
  2. Connecticut: الرموز الجغرافية 1014778 و1014743 و1014843، بما يتوافق مع ثلاث مدن في كونيتيكت
  3. Florida: الرمز الجغرافي 21142

الاستهداف القريب

يمكن تطبيق قواعد الحملة التي تستخدم Matching Geo Targets على المواقع الجغرافية المستهدفة أو المواقع القريبة المستهدفة أو كليهما باستخدام علامة TARGETING.

يتطابق الاستهداف حسب الموقع الجغرافي مع رمز جغرافي يتضمّن رقم تعريف الموقع الجغرافي.

يتحقق الاستهداف القريب من وجود إحداثيات خط العرض وخط الطول المحددة ضمن نصف القرب باستخدام صيغة هافيرسين.

منطق النص البرمجي

يبدأ النص بقراءة القواعد من جميع الأوراق الثلاث. ثم يحاول تنفيذ كل قاعدة من ورقة الحملة بالتسلسل.

بالنسبة إلى كل قاعدة تم تنفيذها، يتحقّق النص البرمجي مما إذا كانت الحملة تستهدف الموقع الجغرافي المحدّد. وإذا كان الأمر كذلك، يسترد النص البرمجي معدِّل عرض السعر الحالي.

وبعد ذلك، يتم استرداد أحوال الطقس لهذا الموقع عن طريق إجراء اتصال بواجهة برمجة التطبيقات OpenWeatherMap. وتُقيّم بعد ذلك قواعد حالة الطقس لمعرفة ما إذا كانت حالة الطقس للموقع يتطابق مع ما تم تحديده في القاعدة. في حال حدوث ذلك، وكان معدِّل عروض الأسعار الجديد يختلف عن معدِّل عروض الأسعار الحالي، يعدّل النص البرمجي معدِّل عروض الأسعار لذلك الموقع الجغرافي.

لا يتم إجراء أي تغييرات إذا لم تتطابق حالة الطقس، أو إذا كانت قيم معدِّلات عروض الأسعار متطابقة، أو إذا كانت قيمة تطبيق المعدّل على للقاعدة هي Matching Geo Targets، لكن الحملة لا تستهدف المواقع الجغرافية التي تم ربطها بالقاعدة.

الإعداد

  • يُرجى التسجيل للحصول على مفتاح واجهة برمجة التطبيقات على openweathermap.org.
  • أنشئ نسخة من نموذج جدول البيانات وعدِّل قواعد حملتك والطقس.
  • أنشئ نصًا برمجيًا جديدًا باستخدام رمز المصدر أدناه.
  • عدِّل المتغيرات OPEN_WEATHER_MAP_API_KEY وSPREADSHEET_URL وTARGETING في النص البرمجي.
  • حدِّد موعدًا لتشغيل الحدث على النحو المطلوب.

رمز مصدر

// 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');
  }
}