날씨별 입찰가

입찰 아이콘

특정 제품 및 서비스의 수요는 날씨에 따라 크게 달라집니다. 예를 들어 춥고 비가 내리기보다는 덥고 화창한 날에 사용자가 놀이공원에 관한 정보를 검색할 가능성이 훨씬 높습니다. 놀이공원 회사는 날씨가 좋을 때 입찰가를 높이고 싶을 수 있지만, 그렇게 하려면 매일 많은 수작업을 해야 합니다. 그러나 Google Ads 스크립트를 사용하면 날씨 정보를 프로그래매틱 방식으로 가져와서 몇 분 안에 입찰가를 조정할 수 있습니다.

이 스크립트는 Google 스프레드시트를 사용하여 캠페인 목록과 관련 위치를 저장합니다. 각 위치에 대해 OpenWeatherMap API 호출을 실행하고 몇 가지 기본 규칙을 사용하여 날씨 조건을 계산합니다. 규칙이 true로 평가되면 해당 위치 입찰 배율이 캠페인의 위치 타겟팅에 적용됩니다.

사용 방법

스크립트는 스프레드시트에서 데이터를 읽는 방식으로 작동합니다. 스프레드시트는 세 개의 개별 시트로 구성됩니다.

1. 캠페인 데이터

규칙 집합은 날씨 조건이 충족될 때 캠페인에 적용될 입찰가 조정을 결정합니다. 필수 열은 다음과 같습니다.

  • 캠페인 이름: 수정할 캠페인의 이름입니다.
  • 날씨 위치: 기상 조건을 확인할 위치입니다.
  • 기상 조건: 이 규칙이 적용될 날씨 조건입니다.
  • 입찰 조정: 날씨 조건이 충족될 때 적용될 위치 입찰가 조정입니다.
  • 수정자 적용 대상: 입찰 조정을 날씨 위치와 일치하는 캠페인 지역 타겟에만 적용할지 아니면 모든 캠페인 지역 타겟에 적용할지 여부를 나타냅니다.
  • 사용 설정됨: 규칙을 사용 설정하려면 Yes를 지정하고 규칙을 사용 중지하려면 No를 지정합니다.

다음 예에는 세 개의 캠페인이 있습니다.

스프레드시트 스크린샷, 시트 1

테스트 캠페인 1은 일반적인 사용 시나리오를 보여줍니다. 캠페인은 메사추세츠주 보스턴을 타겟팅하며 다음과 같은 두 가지 규칙이 있습니다.

  1. 매사추세츠 주 보스턴의 날씨가 Sunny이면 입찰 조정을 1.3로 적용합니다.
  2. 매사추세츠 주 보스턴의 날씨가 Rainy이면 입찰 조정을 0.8로 적용합니다.

테스트 캠페인 2는 테스트 캠페인 1과 동일한 입찰 규칙을 사용하지만 코네티컷을 타겟팅합니다.

테스트 캠페인 3도 동일한 입찰 규칙을 사용하지만 플로리다를 타겟팅합니다. 플로리다의 날씨 규칙은 캠페인이 명시적으로 타겟팅하는 위치가 아닌 전체 주에 매핑되므로 '수정자 적용 대상'이 All Geo Targets로 설정되어 캠페인이 타겟팅하는 도시가 영향을 받습니다.

2. 날씨 데이터

이 시트는 캠페인 데이터 시트에 사용되는 날씨 조건을 정의합니다. 다음 열은 필수사항입니다.

  • 조건 이름: 날씨 조건 이름입니다 (예: Sunny).
  • 온도: 온도(화씨)
  • 강수: 지난 3시간 동안의 비의 양(밀리미터)입니다.
  • 풍속: 풍속(시속 마일)

스프레드시트 스크린샷, 시트 2

위에 표시된 시트는 다음 두 가지 날씨 조건을 정의합니다.

  1. Sunny: 온도가 화씨 65~80도 사이, 지난 3시간 동안 강수량이 1mm 미만이며 풍속이 5mph 미만인 경우
  2. Rainy: 지난 3시간 동안 강수량이 0mm 이상이고 풍속이 10mph 미만일 때

기상 상황

날씨 조건을 정의할 때 다음과 같이 값을 지정합니다.

  • below x: 지정된 값은 below x입니다 (예: below 10).
  • above x: 지정된 값은 above x입니다 (예: above 70).
  • x to y: 지정된 값이 xy 사이입니다(예: 65 to 80).

셀을 비워 두면 해당 매개변수는 계산에서 고려되지 않습니다. 따라서 이 예에서는 Rainy 날씨 조건에 빈 온도 열이 있으므로 이 날씨 조건을 계산할 때 온도를 고려하지 않습니다.

기상 조건을 계산할 때 기상 조건은 AND로 함께 처리됩니다. 이 예에서 Sunny 날씨 조건은 다음과 같이 평가됩니다.

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

3. 날씨 위치 데이터

이 시트는 캠페인 데이터 시트에 사용되는 날씨 위치를 정의하며 다음 두 열로 구성됩니다.

  • 날씨 위치: OpenWeatherMap API에서 이해하는 날씨 위치 이름입니다.
  • 지역 타겟 코드: Google Ads에서 이해하는 지역 타겟팅 코드입니다.

스크립트를 사용하면 단일 날씨 위치에 대해 여러 지역 타겟팅 코드를 지정할 수 있습니다. 날씨 위치가 Google Ads에서 사용할 수 있는 타겟팅 옵션만큼 세분화되지 않을 때도 있기 때문입니다. 단일 날씨 위치를 여러 지리적 위치에 매핑하려면 날씨 위치는 동일하지만 행마다 지역 코드가 다른 행이 여러 개 있으면 됩니다.

스프레드시트 스크린샷, 시트 3

이 예에는 세 개의 날씨 위치가 정의되어 있습니다.

  1. Boston, MA: 지역 코드 10108127
  2. Connecticut: 코네티컷의 3개 도시에 해당하는 지역 코드 1014778, 1014743, 1014843
  3. Florida: 지역 코드 21142

인접 타겟팅

Matching Geo Targets를 사용하는 캠페인 규칙은 TARGETING 플래그를 사용하여 타겟팅된 위치타겟팅된 인접 지역 또는 둘 다에 적용할 수 있습니다.

위치 타겟팅은 지역 코드와 위치 ID를 매칭합니다.

근접성 타겟팅은 haversine 수식을 사용하여 지정된 위도 및 경도 좌표가 인접 반경 내에 있는지 확인합니다.

스크립트 로직

스크립트는 세 시트 모두에서 규칙을 읽는 것으로 시작합니다. 그런 다음 캠페인 시트에서 각 규칙을 순서대로 실행하려고 시도합니다.

실행된 각 규칙에 대해 스크립트는 캠페인이 지정된 위치를 타겟팅하는지 확인합니다. 입찰하는 경우 스크립트가 현재 입찰 조정을 검색합니다.

그런 다음 OpenWeatherMap API를 호출하여 해당 위치의 기상 조건을 가져옵니다. 그런 다음 날씨 조건 규칙을 평가하여 위치의 날씨 조건이 규칙에 지정된 조건과 일치하는지 확인합니다. 조건이 일치하고 새 입찰가 조정이 현재 입찰가 조정 기능과 다르면 스크립트가 해당 위치의 입찰가 조정을 수정합니다.

날씨 조건이 일치하지 않거나 입찰 조정 값이 같거나 규칙의 수정자 적용 대상Matching Geo Targets이지만 캠페인이 규칙에 매핑된 위치를 타겟팅하지 않는 경우 변경되지 않습니다.

설정

  • openweathermap.org에 API 키를 등록합니다.
  • 템플릿 스프레드시트를 복사하고 캠페인 및 날씨 규칙을 수정합니다.
  • 아래의 소스 코드로 새로운 스크립트를 만듭니다.
  • 스크립트에서 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');
  }
}