/**
* Keyword Discovery Script
* @author Charles Bannister
* Surface new keyword opportunities
* Purchased from shabba.io and not for resale or redistribution - thanks!
* @version 1.0.0
* Free updates & support at https://shabba.io/script/9
*
**/
// Template: https://docs.google.com/spreadsheets/d/1yCg93xb3Yhx1c8q9AIbA9KHfa5cyMIquzvA5GW3OOH4
// File > Make a copy or visit https://docs.google.com/spreadsheets/d/1yCg93xb3Yhx1c8q9AIbA9KHfa5cyMIquzvA5GW3OOH4/copy
let INPUT_SHEET_URL = "YOUR_SPREADSHEET_URL_HERE";
try {
module.exports = {
containsStringsInSearchTerm,
containsStringsNotInSearchTerm,
fuzzyMatchScore
};
} catch (e) {
console.log("")
}
const INPUT_TAB_NAME = "Settings";
const SCRIPT_NAME = "Keyword Discovery"
const IGNORED_TERMS_SHEET_NAME = "Ignored Terms"
const NUMBER_OF_FILTERS = 6;
//set to true to send emails regardless of preview status
const OVERRIDE_PREVIEW_EMAIL = false;
//No need to edit anything below this line
let h = new Helper();
let s = new Setting();
const OPTIONS = { includeZeroImpressions: true };
const SINGLE_ACCOUNT_HEADER_TYPES = {
NAME: "normal",
EMAILS: "csv",
FLAG: "bool",
EMAIL_ALERT: "bool",
TAB_NAME: "normal",
N: "normal",
SEARCH_TERM_CONTAINS: 'csv',
SEARCH_TERM_NOT_CONTAINS: 'csv',
FUZZY_THRESHOLD: "normal",
};
const SINGLE_ACCOUNT_HEADER_TYPES_AFTER = {
LOG_SHEET_URL: "normal",
LOGS_COLUMN: "normal",
};
const CUSTOM_METRICS = {
//metric, metric, operator (divide or multiply, whether high or low is good)
Ctr: ["Clicks", "Impressions", "divide", "high"],
Roas: ["ConversionValue", "Cost", "divide", "high"],
Cos: ["Cost", "ConversionValue", "divide", "low"],
Cpa: ["Cost", "Conversions", "divide", "low"],
AverageCpc: ["Cost", "Clicks", "divide", "low"],
ConversionRate: ["Conversions", "Clicks", "divide", "high"],
Rpc: ["ConversionValue", "Clicks", "divide", "high"],
};
function runScript(SETTINGS) {
log("Script Started");
SETTINGS.SPREADSHEET = SpreadsheetApp.openByUrl(INPUT_SHEET_URL);
SETTINGS.CONTROL_SHEET = SETTINGS.SPREADSHEET.getSheetByName(INPUT_TAB_NAME);
checkSettings(SETTINGS);
processRowSettings(SETTINGS);
log(`This row's settings: ${JSON.stringify(SETTINGS)}`);
let searchTermsToIgnore = getSearchTermsToIgnoreFromSheet(SETTINGS);
// log(`searchTermsToIgnore: ${searchTermsToIgnore}`);
addIgnoredSearchTermsToSheet(searchTermsToIgnore, SETTINGS);
let keywords = getKeywords();
// log(`keywords: ${keywords}`);
populateOutputSheet(keywords, SETTINGS);
SETTINGS.LOGS.push("The script ran successfully");
updateControlSheet("", SETTINGS);
log("Finished row " + SETTINGS.ROW_NUM);
}
/**
* query the api for search terms
* populate the sheet
* don't add search terms from the ignored list
* @param {Object} SETTINGS
*/
function populateOutputSheet(keywords, SETTINGS) {
log("clearing output sheet");
let sheet_operations = new SheetOperations(SETTINGS);
let sheet = sheet_operations.getSheetByName(SETTINGS["TAB_NAME"]);
if (sheet.getMaxRows() > 3) {
sheet.deleteRows(4, parseInt(sheet.getMaxRows()) - 3);
}
SETTINGS["FILTER_MAP"] = getFilterMap(SETTINGS);
let cols = [
"AdGroupId",
"CampaignId",
"CampaignName",
"AdGroupName",
"Query",
"ConversionValue",
"Impressions",
"Clicks",
"Cost",
"Conversions",
"TopImpressionPercentage",
"AbsoluteTopImpressionPercentage",
"QueryTargetingStatus",
];
let reportName = "SEARCH_QUERY_PERFORMANCE_REPORT";
let query = [
"select",
cols.join(","),
"from",
reportName,
getQueryWhereString(SETTINGS),
"during",
SETTINGS.DATE_RANGE,
].join(" ");
log(reportName + " query: " + query);
let logArray = [];
let reportIter = AdWordsApp.report(query, OPTIONS).rows();
if (!reportIter.hasNext()) {
log("No search terms found (initial API query)")
}
// AdWordsApp.report(query, OPTIONS).exportToSheet(SpreadsheetApp.openByUrl(INPUT_SHEET_URL).getSheetByName("Sheet5"))
while (reportIter.hasNext()) {
let row = reportIter.next();
row.Impressions = parseInt(row.Impressions, 10);
row.Clicks = parseInt(row.Clicks, 10);
row.Conversions = parseFloat(row.Conversions.toString().replace(/,/g, ""));
row.Cost = parseFloat(row.Cost.toString().replace(/,/g, ""));
row.ConversionValue = parseFloat(
row.ConversionValue.toString().replace(/,/g, "")
);
row = addCustomMetricsToRow(row);
if (skipEntity(row, SETTINGS)) {
continue;
}
if (!containsStringsInSearchTerm(row.Query, SETTINGS)) {
// console.log(`skipping due to contains string logic`)
continue;
}
if (!containsStringsNotInSearchTerm(row.Query, SETTINGS)) {
// console.log(`skipping due to NOT contains string logic`)
continue;
}
if (fuzzyStringInArray(row.Query, keywords, SETTINGS['FUZZY_THRESHOLD'])) {
continue;
}
let logRow = [
row.Query,
row.QueryTargetingStatus,
false,
row.Impressions,
row.Clicks,
row.Ctr,
row.Cost,
row.Conversions,
row.Cpa,
row.ConversionRate,
row.ConversionValue,
row.Roas,
];
logArray.push(logRow);
}
log(logArray.length + " queries found");
if (logArray.length === 0) {
return
};
sheet_operations.writeToSheet(logArray, SETTINGS["TAB_NAME"], 4);
postWriteOperations(logArray, SETTINGS);
if (SETTINGS["EMAIL_ALERT"]) {
sendEmailAlert(SETTINGS, logArray.length);
}
}
/**
* check if the search term contains the required strings
* @param {string} searchTerm
* @param {object} SETTINGS
*/
function containsStringsInSearchTerm(searchTermToCheck, SETTINGS) {
if (SETTINGS.SEARCH_TERM_CONTAINS.length === 0) {
return true;
}
for (let searchTermContainsString of SETTINGS.SEARCH_TERM_CONTAINS) {
if (searchTermToCheck.indexOf(searchTermContainsString) === -1) {
return false;
}
}
return true;
}
/**
* check the search term does not contain the required strings
* @param {string} searchTerm
* @param {object} SETTINGS
*/
function containsStringsNotInSearchTerm(searchTermToCheck, SETTINGS) {
if (SETTINGS.SEARCH_TERM_NOT_CONTAINS.length === 0) {
return true;
}
for (let searchTermNotContainsString of SETTINGS.SEARCH_TERM_NOT_CONTAINS) {
if (searchTermToCheck.indexOf(searchTermNotContainsString) > -1) {
return false;
}
}
return true;
}
function getKeywords() {
const MIN_KEYWORD_IMPRESSIONS = 0;
const DATE_RANGE = 'LAST_30_DAYS'
let query = "SELECT CampaignId, AdGroupId, Criteria, KeywordMatchType " +
"FROM KEYWORDS_PERFORMANCE_REPORT " +
"WHERE Status = ENABLED AND IsNegative = FALSE AND Impressions > " +
MIN_KEYWORD_IMPRESSIONS +
"DURING " +
DATE_RANGE
var keywordReport = AdsApp.report(query);
var keywordRows = keywordReport.rows();
let keywords = [];
while (keywordRows.hasNext()) {
var row = keywordRows.next();
keywords.push(row.Criteria);
}
return keywords;
}
/**
*
* @param {object} SETTINGS
* @returns {array} queries to ignore
*/
function getSearchTermsToIgnoreFromSheet(SETTINGS) {
let outputSheet = SETTINGS.OUTPUT_SHEET;
let data = outputSheet.getDataRange().getValues();
data.shift();
data.shift();
let header = data.shift();
// log(`header: ${header}`)
let searchTermsToIgnore = []
for (let dataIndex in data) {
let row = data[dataIndex];
let query = String(row[header.indexOf("Search Term")]);
let this_is_ok = row[header.indexOf("Ignore")];
// log(`this_is_ok: ${this_is_ok}, query: ${query}`)
if (!this_is_ok) {
continue
}
searchTermsToIgnore.push(query)
}
return searchTermsToIgnore;
}
function sendEmailAlert(SETTINGS, numberOfSearchTerms) {
if (SETTINGS.PREVIEW_MODE && !OVERRIDE_PREVIEW_EMAIL) return;
SETTINGS.LOGS.push(`Rule name: ${SETTINGS["NAME"]}`);
SETTINGS.LOGS.push(`${numberOfSearchTerms} search terms matched the rule`);
//Send email
let SUB = AdWordsApp.currentAccount().getName() + " - New Keyword Discovery Alert";
let MSG =
`Hi,
The ${SCRIPT_NAME} script has results. Here are the logs:
`;
MSG += "
";
for (let l in SETTINGS.LOGS) {
MSG += "- ";
MSG += SETTINGS.LOGS[l];
MSG += "
";
}
MSG += "
";
MSG +=
"
Visit the Google Sheet to review the terms:
" +
SETTINGS.LOG_SHEET_URL;
MSG += "
Thanks,";
MSG += "
Charles";
MSG += "
shabba.io";
let emails = SETTINGS.EMAILS;
for (let i in emails) {
MailApp.sendEmail({
to: emails[i],
subject: SUB,
htmlBody: MSG,
});
}
}
function updateControlSheet(errorMessage, SETTINGS) {
//remove the unsuccessful note we added
SETTINGS.CONTROL_SHEET.getRange(
SETTINGS.ROW_NUM,
SETTINGS.LOGS_COLUMN,
1,
1
).setNote("");
if (errorMessage != "") {
SETTINGS.CONTROL_SHEET.getRange(
SETTINGS.ROW_NUM,
SETTINGS.LOGS_COLUMN,
1,
2
).setValues([[errorMessage, SETTINGS.NOW]]);
SETTINGS.CONTROL_SHEET.getRange(
SETTINGS.ROW_NUM,
SETTINGS.LOGS_COLUMN,
1,
1
).setNote(
"Note: Some rows running on account ID " +
SETTINGS.ID +
" may not have completed sucessfully. Please see their respective logs and 'Last Run' times."
);
throw errorMessage;
}
//add final logs
//stringify logs
logString = h.stringifyLogs(SETTINGS.LOGS);
//update control sheet
let put = [logString, SETTINGS.NOW];
SETTINGS.CONTROL_SHEET.getRange(
SETTINGS.ROW_NUM,
SETTINGS.LOGS_COLUMN,
1,
2
).setValues([put]);
SETTINGS.CONTROL_SHEET.getRange(
SETTINGS.ROW_NUM,
SETTINGS.LOGS_COLUMN,
1,
1
).setNote(logString);
}
function swapLabelTextForIds(SETTINGS) {
//swap label text for label ids
for (let key in SETTINGS) {
if (key.indexOf("FILTER") === -1 || key.indexOf("METRIC") === -1) {
continue;
}
if (SETTINGS[key] !== "Labels") continue;
// log(key + " - " + SETTINGS[key]);
let filter_value = SETTINGS[key.replace("METRIC", "VALUE")];
let value_split = String(filter_value).split(",");
for (let v in value_split) {
value_split[v] = getLabelIdFromString(value_split[v]);
}
SETTINGS[key.replace("METRIC", "VALUE")] = value_split.join(",");
}
return SETTINGS;
}
function getFilterWhereString(SETTINGS) {
//custom metrics map
//name : formula, whether high or low is good (e.g. ROAS high is good, CPA high is bad - used for Vs target calcs)
let filters = Object.keys(SETTINGS).map(function (x) {
if (x.indexOf("FILTER") > -1) {
return x;
}
});
let filterMap = {};
let filterParts = ["metric", "operator", "value"];
let numberOfFilters = filters.length / filterParts.length;
// log("number of filters: " + numberOfFilters)
for (let i = 0; i < numberOfFilters; i++) {
let filterName = "FILTER_" + (i + 1);
if (Object.keys(SETTINGS).indexOf(filterName + "_METRIC") == -1) continue;
if (SETTINGS[filterName + "_METRIC"] == "") continue;
filterMap[filterName] = filterMap[filterName] || {};
for (let x in filterParts) {
filterMap[filterName][filterParts[x]] =
SETTINGS[filterName + "_" + filterParts[x].toUpperCase()];
}
}
let where = "";
let whereArray = [];
//turn the filters object into a where statement string
whereArray.push(filtersToWhereStatement(filterMap, filterParts));
function filtersToWhereStatement(filterMap, filterParts) {
let str = [];
for (let filter in filterMap) {
//if the metric is a custom metric, continue
if (Object.keys(CUSTOM_METRICS).indexOf(filterMap[filter]["metric"]) > -1)
continue;
str.push("and");
for (let p in filterParts) {
str.push(filterMap[filter][filterParts[p]]);
}
}
return str.join(" ");
}
where += whereArray.join(" ");
return where;
}
function getLabelIdFromString(str) {
let labels = AdsApp.labels()
.withCondition("Name = '" + str.trim() + "'")
.get();
if (labels.hasNext()) {
let label = labels.next();
// log(label.getId())
return label.getId();
} else {
checkLabel(str);
return "0000";
}
}
//if the label doesn't exist, create it
function checkLabel(labelName) {
if (labelName == "") {
return;
}
let labels = AdWordsApp.labels().get();
let exists = false;
while (labels.hasNext()) {
let label = labels.next();
if (label.getName() == labelName) {
exists = true;
}
}
if (!exists) {
AdWordsApp.createLabel(labelName);
}
}
function getQueryWhereString(SETTINGS) {
let where = "where CampaignStatus = ENABLED and AdGroupStatus = ENABLED ";
if (SETTINGS.QUERY_TARGETING_STATUS) {
where += " and QueryTargetingStatus = " + SETTINGS.QUERY_TARGETING_STATUS;
}
let whereArray = [];
for (let i in SETTINGS.CAMPAIGN_NAME_CONTAINS) {
whereArray.push(
" and CampaignName CONTAINS_IGNORE_CASE '" +
SETTINGS.CAMPAIGN_NAME_CONTAINS[i].trim() +
"'"
);
}
for (let i in SETTINGS.CAMPAIGN_NAME_NOT_CONTAINS) {
whereArray.push(
" and CampaignName DOES_NOT_CONTAIN_IGNORE_CASE '" +
SETTINGS.CAMPAIGN_NAME_NOT_CONTAINS[i].trim() +
"'"
);
}
where += whereArray.join(" ");
return where + " " + getFilterWhereString(SETTINGS);
}
function addCustomMetricsToRow(row) {
//CUSTOM_METRICS
for (let metricName in CUSTOM_METRICS) {
if (valueUndefined(row[CUSTOM_METRICS[metricName][1]]))
throw "Error: Can't find the metric " + CUSTOM_METRICS[metricName][1];
if (valueUndefined(row[CUSTOM_METRICS[metricName][0]]))
throw "Error: Can't find the metric " + CUSTOM_METRICS[metricName][0];
if (CUSTOM_METRICS[metricName][2] == "divide") {
row[metricName] =
row[CUSTOM_METRICS[metricName][0]] == 0 ||
row[CUSTOM_METRICS[metricName][1]] == 0
? 0
: roundFloatToTwoDecimalPlaces(
row[CUSTOM_METRICS[metricName][0]] /
row[CUSTOM_METRICS[metricName][1]],
4
);
}
if (CUSTOM_METRICS[metricName][2] == "multiply") {
row[metricName] =
row[CUSTOM_METRICS[metricName][0]] == 0 ||
row[CUSTOM_METRICS[metricName][1]] == 0
? 0
: roundFloatToTwoDecimalPlaces(
row[CUSTOM_METRICS[metricName][0]] *
row[CUSTOM_METRICS[metricName][1]],
4
);
}
}
return row;
}
function postWriteOperations(logArray, SETTINGS) {
let sheet_operations = new SheetOperations(SETTINGS);
let sheet = sheet_operations.getSheetByName(SETTINGS["TAB_NAME"]);
sheet
.getRange("A2")
.setValue(
"Current Data For Lookback (" +
SETTINGS.N +
" days)" +
" - " +
SETTINGS.DATE_RANGE
);
let header = sheet.getRange(3, 1, 1, sheet.getLastColumn()).getValues();
addValidation(logArray.length, sheet, header, sheet_operations);
sheet_operations.sortColumn(sheet, parseInt(header[0].indexOf("Cost")) + 1);
}
function addValidation(rows, sheet, header, sheet_operations) {
//add validaiton to the keywords sheet
let checkbox_cols = [];
for (let value_index in header[0]) {
let value = header[0][value_index];
if (value == "Ignore") {
checkbox_cols.push(parseInt(value_index) + 1)
};
}
let start_row = 4;
// log(`checkbox_cols: ${checkbox_cols}`)
for (let checkbox_cols_index in checkbox_cols) {
sheet_operations.addCheckBox(
start_row,
checkbox_cols[checkbox_cols_index],
rows,
sheet
);
}
}
class SheetOperations {
constructor(SETTINGS) {
this.SETTINGS = SETTINGS;
}
addDropdownValidation(validation_list, sheet, start_row, rows, column) {
let rule = SpreadsheetApp.newDataValidation()
.requireValueInList(validation_list)
.build();
sheet.getRange(start_row, column, rows, 1).setDataValidation(rule);
}
addCheckBox(start_row, column, rows, sheet) {
let range = sheet.getRange(start_row, column, rows, 1);
let enforceCheckbox = SpreadsheetApp.newDataValidation();
enforceCheckbox.requireCheckbox();
enforceCheckbox.setAllowInvalid(false);
enforceCheckbox.build();
range.setDataValidation(enforceCheckbox);
}
getSheetByName(name) {
// console.log("Getting sheet by name: " + name)
try {
return SpreadsheetApp.openByUrl(INPUT_SHEET_URL).getSheetByName(name);
} catch (e) {
let sheets = SpreadsheetApp.openByUrl(INPUT_SHEET_URL).getSheets();
let sheet_names = [];
for (let sheet_key in sheets) {
let sheet = sheets[sheet_key];
sheet_names.push(sheet.getName());
}
log("There was a problem getting sheet '" + name + "'");
log("Here are the availble sheets: " + sheet_names.join(", "));
log("And here's the error: ");
throw e;
}
}
appendToSheet(logArray, sheetName, start_row) {
if (logArray.length === 0) return;
let sheet = this.getSheetByName(sheetName);
sheet.insertRowsAfter(start_row - 1, logArray.length);
let max_rows = 20000;
if (sheet.getLastRow() > max_rows) {
sheet.deleteRows(max_rows, sheet.getLastRow() - max_rows);
}
sheet
.getRange(start_row, 1, logArray.length, logArray[0].length)
.setValues(logArray);
}
getAllRowsData(sheet, start_row, num_columns) {
let start_column = 1;
return sheet
.getRange(start_row, start_column, sheet.getLastRow(), num_columns)
.getValues();
}
writeToSheet(logArray, sheetName, start_row) {
log("Writing to " + sheetName);
if (logArray.length === 0) return;
let sheet = this.getSheetByName(sheetName);
let lastRow = this.getLastRow(sheet, 4);
// log("lastRow: " + lastRow)
if (lastRow < parseInt(logArray.length) + 20) {
sheet.insertRowsAfter(lastRow, lastRow);
}
log("writing to sheet");
sheet
.getRange(4, 1, logArray.length, logArray[0].length)
.setValues(logArray);
// Utilities.sleep(50000)
}
clearRange(sheet, row, column, rows, columns) {
let values = [];
for (let r = 0; r < rows; r++) {
let this_row = [];
for (let c = 0; c < columns; c++) {
this_row.push("");
}
values.push(this_row);
}
sheet.getRange(row, column, rows, columns).setValues(values);
}
/*Get the last row based on the first column's values */
getLastRow(sheet, startRow) {
let row = startRow;
while (sheet.getRange(row, 1).getValue()) {
row++;
}
row = row - 1;
if (row < 1) return 1;
return row;
}
addNumberFormat(sheet_name, column, start_row) {
// sheet.getRange(1,2,sheet.getLastRow(),sheet.getLastColumn()).setNumberFormat("0.00")
// sheet.getRange(1,13,sheet.getLastRow(),sheet.getLastColumn()).setNumberFormat("0.00%")
// sheet.getRange(1,7,sheet.getLastRow(),1).setNumberFormat("0.00%")
// sheet.getRange(1,10,sheet.getLastRow(),1).setNumberFormat("0.00%")
}
sortColumn(sheet, columnNumber) {
let startRow = 4;
if (sheet.getLastRow() - startRow < 1) {
return
}
sheet
.getRange(
startRow,
1,
sheet.getLastRow() - startRow,
sheet.getLastColumn()
)
.sort({ column: columnNumber, ascending: false });
}
}
function getFilterMap(SETTINGS) {
let filters = Object.keys(SETTINGS).map(function (x) {
if (x.indexOf("FILTER") > -1) {
return x;
}
});
let filterMap = {};
let filterParts = ["metric", "operator", "value"];
let numberOfFilters = filters.length / filterParts.length;
// log("number of filters: " + numberOfFilters)
for (let i = 0; i < numberOfFilters; i++) {
let filterName = "FILTER_" + (i + 1);
if (Object.keys(SETTINGS).indexOf(filterName + "_METRIC") == -1) continue;
if (SETTINGS[filterName + "_METRIC"] == "") {
continue;
}
filterMap[filterName] = filterMap[filterName] || {};
for (let x in filterParts) {
filterMap[filterName][filterParts[x]] =
SETTINGS[filterName + "_" + filterParts[x].toUpperCase()];
}
}
return filterMap;
}
//Whether to skip the entity based on the stats and filters
//Only check custom metrics e.g. cpa
function skipEntity(row, SETTINGS) {
//custom metrics map
//name : formula, whether high or low is good (e.g. ROAS high is good, CPA high is bad - used for Vs target calcs)
let filterMap = SETTINGS["FILTER_MAP"];
// log(JSON.stringify(filterMap))
// log(JSON.stringify(row))
for (let filter in filterMap) {
let this_filter = filterMap[filter];
if (filterNotInCustomMetrics(this_filter.metric)) continue;
let eval_string =
row[this_filter.metric] +
" " +
this_filter.operator +
" " +
String(this_filter.value);
try {
if (!eval(eval_string)) {
return true;
}
} catch (e) {
log("eval_string: " + eval_string);
throw e;
}
}
function filterNotInCustomMetrics(metric) {
return Object.keys(CUSTOM_METRICS).indexOf(metric) === -1;
}
return false;
}
function valueUndefined(val) {
let result = false;
if (val === 0) return false;
if (!val) return true;
if (typeof val === "undefined") return true;
return result;
}
function calculateCpaBid(bid, target, actual, min) {
if (actual === 0) return min;
return bid + bid * getChangePercentage(actual, target);
}
function calculateRoasBid(bid, target, actual, min) {
if (actual === 0) return min;
return bid + bid * getChangePercentage(target, actual);
}
/**
* add positive and negative keywords
* @returns nothing
**/
function addIgnoredSearchTermsToSheet(searchTermsToIgnore, SETTINGS) {
if (searchTermsToIgnore.length === 0) {
return;
}
let logArray = [];
//for each search term add to the logArray array along with a date
for (let i in searchTermsToIgnore) {
let row = [];
row.push(searchTermsToIgnore[i]);
row.push(SETTINGS.NOW);
logArray.push(row);
}
const sheet_operations = new SheetOperations(SETTINGS);
sheet_operations.appendToSheet(logArray, IGNORED_TERMS_SHEET_NAME, 5);
SETTINGS.LOGS.push(
parseInt(searchTermsToIgnore.length) + " search terms added to the ignore list"
);
}
function addMatchType(text, matchType) {
matchType = matchType.toLowerCase();
if (matchType === "exact") return "[" + text + "]";
if (matchType === "phrase") return '"' + text + '"';
if (matchType === "broad") return text;
throw "Error: Match type not recognised";
//text.replace("[","").replace("]","").toLowerCase();
}
function isToday(date) {
return date.getDate() == new Date().getDate();
}
//update the change log
//return false if the Id has already been updated today
function updateChangeLog(CampaignName, AdGroupName, Id, SETTINGS) {
//grab the change log
//if the id does not exist in the sheet, update the sheet and return true
//if the id exists and it's today, return false
//if the id exists but it's not today, update the sheet and return true
if (AdsApp.getExecutionInfo().isPreview()) return true;
let map = {};
let sheet = SETTINGS.CHANGE_LOG_SHEET;
if (sheet.getLastRow() < 2) {
log("No change log values");
map = updateMap(map, CampaignName, AdGroupName, Id, SETTINGS.NOW);
writeMapToSheet(sheet, map);
return true;
}
//grab the current details and update
let data = sheet.getDataRange().getValues();
data.shift();
for (let d in data) {
let row = data[d];
let campaign = row[0];
let adgroup = row[1];
let id = row[2];
let date = row[3];
map[campaign + adgroup + id] = {};
map[campaign + adgroup + id]["campaign"] = campaign;
map[campaign + adgroup + id]["adgroup"] = adgroup;
map[campaign + adgroup + id]["id"] = id;
map[campaign + adgroup + id]["date"] = date;
}
let this_id = CampaignName + AdGroupName + Id;
if (valueUndefined(map[this_id])) {
// log("Change log id undefined: " + this_id)
// log(JSON.stringify(map))
map = updateMap(map, CampaignName, AdGroupName, Id, SETTINGS.NOW);
writeMapToSheet(sheet, map);
return true;
}
let this_date = new Date(map[this_id]["date"]);
//don't update the bid if the log contains today's date
if (isToday(this_date)) {
// log("Id already updated today")
return false; //do not update
} else {
// log("Value updated but it wasn't today")
map = updateMap(map, CampaignName, AdGroupName, Id, SETTINGS.NOW);
writeMapToSheet(sheet, map);
return true;
}
function writeMapToSheet(sheet, map) {
// log("Adding " + Object.keys(map).length + " rows to the change log")
// log("Writing this map: " + JSON.stringify(map))
sheet.clear();
let header = ["Campaign", "Ad group", "Id", "Last Updated"];
let logArray = [header];
for (let id in map) {
logArray.push([
map[id]["campaign"],
map[id]["adgroup"],
map[id]["id"],
map[id]["date"],
]);
}
writeArrayToSheet(logArray, sheet, 1);
}
function updateMap(map, CampaignName, AdGroupName, Id, NOW) {
let this_id = CampaignName + AdGroupName + Id;
map[this_id] = {};
map[this_id]["campaign"] = CampaignName;
map[this_id]["adgroup"] = AdGroupName;
map[this_id]["id"] = Id;
map[this_id]["date"] = NOW;
return map;
}
}
function getChangePercentage(before, after) {
if (before === 0 && after === 0) return 0;
return (after - before) / before;
}
function parseDateRange(SETTINGS) {
let YESTERDAY = getAdWordsFormattedDate(1, "yyyyMMdd");
SETTINGS.DATE_RANGE =
getAdWordsFormattedDate(SETTINGS.N, "yyyyMMdd") + "," + YESTERDAY;
}
function getActualVsTarget(target_type, actual, target) {
if (target_type === "CPA") {
return target / actual;
}
return actual / target;
}
/*
SETTINGS SECTION
*/
/**
* Get the editors from the sheet
* @param {drive element} - main control (settings) sheet
* @param {int} - Number of the column containing the logs
* @return {array} editors
**/
function getEditorsFromSheet(CONTROL_SHEET, logsColumn) {
let editors = CONTROL_SHEET.getRange(1, logsColumn).getValue();
if (editors == "") {
return;
}
if (editors.indexOf(",") > -1) {
editors = editors.split(",");
for (let e in editors) {
editors[e] = editors[e].trim().toLowerCase();
}
} else {
editors = [editors.trim().toLowerCase()];
}
return editors;
}
function getFilterHeaderTypes() {
let map = {};
for (let i = 1; i < NUMBER_OF_FILTERS + 1; i++) {
map["FILTER_" + i + "_METRIC"] = "normal";
map["FILTER_" + i + "_OPERATOR"] = "filter_operator";
map["FILTER_" + i + "_VALUE"] = "filter_value";
}
return map;
}
function getHeaderTypes() {
let MCC_HEADER_TYPES = {};
if (isMCC()) {
MCC_HEADER_TYPES = { ID: "normal" };
}
let FILTER_HEADER_TYPES = getFilterHeaderTypes();
let HEADER_TYPES = objectMerge(
MCC_HEADER_TYPES,
SINGLE_ACCOUNT_HEADER_TYPES,
FILTER_HEADER_TYPES,
SINGLE_ACCOUNT_HEADER_TYPES_AFTER
);
return HEADER_TYPES;
}
function buildTestSettings() {
let map = {};
for (let key in SINGLE_ACCOUNT_HEADER_TYPES) {
map[key] = "random value";
}
return map;
}
function scanForAccounts() {
log("getting settings...");
log(
"The settings sheet should contain " + NUMBER_OF_FILTERS + " filter sets"
);
let map = {};
let controlSheet =
SpreadsheetApp.openByUrl(INPUT_SHEET_URL).getSheetByName(INPUT_TAB_NAME);
let data = controlSheet.getDataRange().getValues();
data.shift();
data.shift();
data.shift();
HEADER_TYPES = getHeaderTypes();
let HEADER = Object.keys(HEADER_TYPES);
let LOGS_COLUMN = 0;
let col = 5;
while (controlSheet.getRange(3, col).getValue()) {
LOGS_COLUMN = controlSheet.getRange(3, col).getValue() == "Logs" ? col : 0;
if (LOGS_COLUMN > 0) {
break;
}
col++;
}
// log(HEADER)
let flagPosition = HEADER.indexOf("FLAG");
for (let k in data) {
//if "run script" is not set to "yes", continue.
// log(data[k][flagPosition])
if (data[k][0] == "" || data[k][flagPosition].toLowerCase() != "yes") {
continue;
}
let rowNum = parseInt(k, 10) + 4;
let id = data[k][0];
let rowId = id + "/" + rowNum;
map[id] = map[id] || {};
map[id][rowId] = { ROW_NUM: parseInt(k, 10) + 4 };
for (let j in HEADER) {
if (HEADER[j] == "LOGS_COLUMN") {
map[id][rowId][HEADER[j]] = LOGS_COLUMN;
continue;
}
map[id][rowId][HEADER[j]] = data[k][j];
}
}
if (Object.keys(map).length == 0) {
console.warn("No rules were specified");
console.warn(
"To run the script, add settings and set 'Run Script' to 'Yes'"
);
return;
}
let previousOperator = "";
for (let id in map) {
for (let rowId in map[id]) {
for (let key in map[id][rowId]) {
let isFilterValue =
key.indexOf("FILTER") > -1 && key.indexOf("VALUE") > -1;
let filter_metric = !isFilterValue
? ""
: map[id][rowId][key.replace("VALUE", "METRIC")];
map[id][rowId][key] = processSetting(
key,
map[id][rowId][key],
HEADER_TYPES,
controlSheet,
previousOperator,
filter_metric
);
previousOperator =
key.indexOf("OPERATOR") > -1 ? map[id][rowId][key] : "";
}
}
}
return map;
}
function objectMerge() {
for (let i = 1; i < arguments.length; i++)
for (let a in arguments[i]) arguments[0][a] = arguments[i][a];
return arguments[0];
}
function isList(operator) {
let list_operators = [
"IN",
"NOT_IN",
"CONTAINS_ANY",
"CONTAINS_NONE",
"CONTAINS_ALL",
];
return list_operators.indexOf(operator) > -1;
}
// log(formatFilterValue("Example campaign name, another example", "CONTAINS_ANY"))
function formatFilterValue(value, operator, filter_metric) {
if (h.isNumber(value)) return value;
if (filter_metric === "Labels") {
value = String(value)
.split(",")
.map(function (x) {
return getLabelIdFromString(x);
})
.join(",");
}
if (String(value).split(",").length === 1) {
return isList(operator) ? "['" + value + "']" : "'" + value + "'";
}
let arr = String(value).split(",");
value =
arr.length === 1
? arr[0]
: "[" +
arr
.map(function (x) {
return "'" + x.trim() + "'";
})
.join(",") +
"]";
return value;
}
// log(processSetting("FILTER_1", "world", {"FILTER_1":"filter_value"},"controlSheet"))
function processSetting(
key,
value,
HEADER,
controlSheet,
previousOperator,
filter_metric
) {
let type = HEADER[key];
if (key == "ROW_NUM") {
return value;
}
switch (type) {
case "filter_operator":
return value.replace("'", "");
break;
case "filter_value":
value = formatFilterValue(value, previousOperator, filter_metric);
return value;
break;
case "label":
return [
controlSheet
.getRange(3, Object.keys(HEADER).indexOf(key) + 1)
.getValue(),
value,
];
break;
case "normal":
return value;
break;
case "bool":
return value == "Yes" ? true : false;
break;
case "csv":
let ret = value.split(",");
ret = ret[0] == "" && ret.length == 1 ? [] : ret;
if (ret.length == 0) {
return [];
} else {
for (let r in ret) {
ret[r] = String(ret[r]).trim();
}
}
return ret;
break;
default:
throw "error setting type " + type + " not recognised for " + key;
}
}
function processRowSettings(SETTINGS) {
SETTINGS.NOW = Utilities.formatDate(
new Date(),
AdWordsApp.currentAccount().getTimeZone(),
"MMM dd, yyyy HH:mm:ss"
);
SETTINGS["QUERY_TARGETING_STATUS"] =
SETTINGS["QUERY_TARGETING_STATUS"] == "ALL"
? ""
: SETTINGS["QUERY_TARGETING_STATUS"];
SETTINGS.LOGS_COLUMN = getLogsColumn(SETTINGS.CONTROL_SHEET);
SETTINGS.LOGS = [];
SETTINGS.EMAILS_SHEET = true;
let defaultNote =
"Possible problems include: 1) There was an error (check the logs within Google Ads) 2) The script was stopped before completion";
SETTINGS.CONTROL_SHEET.getRange(SETTINGS.ROW_NUM, SETTINGS.LOGS_COLUMN, 1, 1)
.setValue(
"The script is either still running or didn't finish successfully"
)
.setNote(defaultNote);
parseDateRange(SETTINGS);
SETTINGS.PREVIEW_MODE = AdWordsApp.getExecutionInfo().isPreview();
if (SETTINGS.PREVIEW_MODE) {
let msg = "Running in preview mode. No changes will be made.";
SETTINGS.LOGS.push(msg);
}
const spreadsheet = SpreadsheetApp.openByUrl(INPUT_SHEET_URL);
SETTINGS.SPREADSHEET = spreadsheet
SETTINGS.OUTPUT_SHEET = spreadsheet.getSheetByName(String(SETTINGS.TAB_NAME));
SETTINGS.IGNORED_TERMS_SHEET = spreadsheet.getSheetByName(IGNORED_TERMS_SHEET_NAME);
}
/**
* Checks the settings for issues
* @returns nothing
**/
function checkSettings(SETTINGS) {
//check the settings here
if (SETTINGS.N === "") {
updateControlSheet("Please set a lookback window", SETTINGS);
}
if (fuzzyThresholdIsValid(SETTINGS['FUZZY_THRESHOLD']) === false) {
updateControlSheet("Please check the fuzzy threshold", SETTINGS);
}
if (!sheetExists(SETTINGS)) {
const errorMessage = `The sheet ${SETTINGS["TAB_NAME"]} does not exist. Please create it by duplicating an existing sheet (aka tab).`
throw (errorMessage)
}
}
function sheetExists(SETTINGS) {
let sheets = SETTINGS.SPREADSHEET.getSheets();
for (let sheet of sheets) {
if (String(sheet.getName()) === String(SETTINGS["TAB_NAME"])) {
return true;
}
}
return false;
}
/**
* Get AdWords Formatted date for n days back
* @param {drive element} - drive element (such as folder or spreadsheet)
* @param {drive element} - main control (settings) sheet
* @param {int} - Number of the column containing the logs
* @return nothing
**/
function addEditors(spreadsheet, controlSheet, LOGS_COLUMN) {
//check current editors, add if they don't exist
let currentEditors = spreadsheet.getEditors();
let currentEditorEmails = [];
for (let c in currentEditors) {
currentEditorEmails.push(currentEditors[c].getEmail().trim().toLowerCase());
}
let editors = controlSheet.getRange(1, LOGS_COLUMN).getValue();
if (editors == "") {
return;
}
if (editors.indexOf(",") > -1) {
editors = editors.split(",");
for (let e in editors) {
editors[e] = editors[e].trim().toLowerCase();
}
} else {
editors = [editors.trim().toLowerCase()];
}
for (let e in editors) {
let index = currentEditorEmails.indexOf(editors[e]);
if (currentEditorEmails.indexOf(editors[e]) == -1) {
spreadsheet.addEditor(editors[e]);
}
}
}
/*
SET AND FORGET FUNCTIONS
*/
function getLogsColumn(controlSheet) {
let col = 5;
let LOGS_COLUMN = 0;
while (String(controlSheet.getRange(3, col).getValue())) {
LOGS_COLUMN = controlSheet.getRange(3, col).getValue() == "Logs" ? col : 0;
if (LOGS_COLUMN > 0) {
break;
}
col++;
}
return LOGS_COLUMN;
}
function callBack() {
// Do something here
Logger.log("Finished");
}
function stringifyLogs(logs) {
let s = "";
for (let l in logs) {
s += parseInt(l) + 1 + ") ";
s += logs[l] + " ";
}
return s;
}
/**
* Get AdWords Formatted date for n days back
* @param {int} d - Numer of days to go back for start/end date
* @return {String} - Formatted date yyyyMMdd
**/
function getAdWordsFormattedDate(d, format) {
let date = new Date();
date.setDate(date.getDate() - d);
return Utilities.formatDate(
date,
AdWordsApp.currentAccount().getTimeZone(),
format
);
}
function log(msg) {
try {
console.log(AdWordsApp.currentAccount().getName() + " - " + msg);
} catch (e) {
log(msg);
}
}
function round(num, n) {
return +(Math.round(parseFloat(num) + "e+" + n) + "e-" + n);
}
function main() {
if (isMCC()) {
let SETTINGS = scanForAccounts();
// log(JSON.stringify(SETTINGS))
let ids = Object.keys(SETTINGS);
if (ids.length == 0) {
Logger.log("No Rules Specified");
return;
}
MccApp.accounts()
.withIds(ids)
.withLimit(50)
.executeInParallel("runRows", "callBack", JSON.stringify(SETTINGS));
} else {
let ALL_SETTINGS = scanForAccounts();
// log(JSON.stringify(ALL_SETTINGS))
//run all rows and all accounts
for (let S in ALL_SETTINGS) {
for (let R in ALL_SETTINGS[S]) {
runScript(ALL_SETTINGS[S][R]);
}
}
}
}
function isMCC() {
try {
MccApp.accounts();
return true;
} catch (e) {
if (String(e).indexOf("not defined") > -1) {
return false;
} else {
return true;
}
}
}
function runRows(INPUT) {
log("running rows");
let SETTINGS =
JSON.parse(INPUT)[AdWordsApp.currentAccount().getCustomerId().toString()];
for (let rowId in SETTINGS) {
runScript(SETTINGS[rowId]);
}
}
function Helper() {
/**
* Check if a string is a number, used when grabbing numbers from the sheet
* If the string contains anything but numbers and a full stop (.) it returns false
* @param {number as a string}
* @returns {bool}
**/
this.isNumber = function (n) {
if (typeof n == "number") return true;
n = String(n).trim();
let digits = n.split("");
for (let d in digits) {
if (digits[d] == ".") {
continue;
}
if (isNaN(digits[d])) {
return false;
}
}
return true;
};
/**
* Calculate ROAS
* @param {number} - Conv. Value
* @param {number} - Cost
* @returns {number}
**/
this.calculateRoas = function (ConversionValue, Cost) {
if (Cost == 0) return 0;
if (ConversionValue == 0) return 0;
if (Cost > ConversionValue) return 0;
return ConversionValue / Cost;
};
/**
* Return the column number of the logs column
* @param {google sheet} control/settings sheet
* @return {number} - Logs column
**/
this.getLogsColumn = function (controlSheet) {
let col = 5;
let LOGS_COLUMN = 0;
while (String(controlSheet.getRange(3, col).getValue())) {
LOGS_COLUMN =
controlSheet.getRange(3, col).getValue() == "Logs" ? col : 0;
if (LOGS_COLUMN > 0) {
break;
}
col++;
}
return LOGS_COLUMN;
};
/**
* Turn an array of logs into a numbered string
* @param {array} logs
* @return {String} - Logs
**/
this.stringifyLogs = function (logs) {
let s = "";
for (let l in logs) {
s += parseInt(l) + 1 + ") ";
s += logs[l] + " ";
}
return s;
};
/**
* Get AdWords Formatted date for n days back
* @param {int} d - Numer of days to go back for start/end date
* @return {String} - Formatted date yyyyMMdd
**/
this.getAdWordsFormattedDate = function (d, format) {
let date = new Date();
date.setDate(date.getDate() - d);
return Utilities.formatDate(
date,
AdWordsApp.currentAccount().getTimeZone(),
format
);
};
this.round = function (num, n) {
return +(Math.round(num + "e+" + n) + "e-" + n);
};
/**
* Add editors to the sheet
* @param {drive element} - drive element (such as folder or spreadsheet)
* @param {array} - editors to add
* @return nothing
**/
this.addEditors = function (spreadsheet, editors) {
//check current editors, add if they don't exist
let currentEditors = spreadsheet.getEditors();
let currentEditorEmails = [];
for (let c in currentEditors) {
currentEditorEmails.push(
currentEditors[c].getEmail().trim().toLowerCase()
);
}
for (let e in editors) {
if (currentEditorEmails.indexOf(editors[e]) == -1) {
spreadsheet.addEditor(editors[e]);
}
}
};
}
//uses helpers.js
function Setting() {
this.parseDateRange = function (SETTINGS) {
let YESTERDAY = h.getAdWordsFormattedDate(1, "yyyyMMdd");
SETTINGS.DATE_RANGE = "20000101," + YESTERDAY;
if (SETTINGS.DATE_RANGE_LITERAL == "LAST_N_DAYS") {
SETTINGS.DATE_RANGE =
h.getAdWordsFormattedDate(SETTINGS.N, "yyyyMMdd") + "," + YESTERDAY;
}
if (SETTINGS.DATE_RANGE_LITERAL == "LAST_N_MONTHS") {
let now = new Date(
Utilities.formatDate(
new Date(),
AdWordsApp.currentAccount().getTimeZone(),
"MMM dd, yyyy HH:mm:ss"
)
);
now.setHours(12);
now.setDate(0);
let TO = Utilities.formatDate(now, "PST", "yyyyMMdd");
now.setDate(1);
let counter = 1;
while (counter < SETTINGS.N) {
now.setMonth(now.getMonth() - 1);
counter++;
}
let FROM = Utilities.formatDate(now, "PST", "yyyyMMdd");
SETTINGS.DATE_RANGE = FROM + "," + TO;
}
};
}
var FuzzySet = (function () {
"use strict";
const FuzzySet = function (
arr,
useLevenshtein,
gramSizeLower,
gramSizeUpper
) {
var fuzzyset = {};
// default options
arr = arr || [];
fuzzyset.gramSizeLower = gramSizeLower || 2;
fuzzyset.gramSizeUpper = gramSizeUpper || 3;
fuzzyset.useLevenshtein =
typeof useLevenshtein !== "boolean" ? true : useLevenshtein;
// define all the object functions and attributes
fuzzyset.exactSet = {};
fuzzyset.matchDict = {};
fuzzyset.items = {};
// helper functions
var levenshtein = function (str1, str2) {
var current = [],
prev,
value;
for (var i = 0; i <= str2.length; i++)
for (var j = 0; j <= str1.length; j++) {
if (i && j)
if (str1.charAt(j - 1) === str2.charAt(i - 1)) value = prev;
else value = Math.min(current[j], current[j - 1], prev) + 1;
else value = i + j;
prev = current[j];
current[j] = value;
}
return current.pop();
};
// return an edit distance from 0 to 1
var _distance = function (str1, str2) {
if (str1 === null && str2 === null)
throw "Trying to compare two null values";
if (str1 === null || str2 === null) return 0;
str1 = String(str1);
str2 = String(str2);
var distance = levenshtein(str1, str2);
if (str1.length > str2.length) {
return 1 - distance / str1.length;
} else {
return 1 - distance / str2.length;
}
};
// u00C0-u00FF is latin characters
// u0621-u064a is arabic letters
// u0660-u0669 is arabic numerals
// TODO: figure out way to do this for more languages
var _nonWordRe = /[^a-zA-Z0-9\u00C0-\u00FF\u0621-\u064A\u0660-\u0669, ]+/g;
var _iterateGrams = function (value, gramSize) {
gramSize = gramSize || 2;
var simplified = "-" + value.toLowerCase().replace(_nonWordRe, "") + "-",
lenDiff = gramSize - simplified.length,
results = [];
if (lenDiff > 0) {
for (var i = 0; i < lenDiff; ++i) {
simplified += "-";
}
}
for (var i = 0; i < simplified.length - gramSize + 1; ++i) {
results.push(simplified.slice(i, i + gramSize));
}
return results;
};
var _gramCounter = function (value, gramSize) {
// return an object where key=gram, value=number of occurrences
gramSize = gramSize || 2;
var result = {},
grams = _iterateGrams(value, gramSize),
i = 0;
for (i; i < grams.length; ++i) {
if (grams[i] in result) {
result[grams[i]] += 1;
} else {
result[grams[i]] = 1;
}
}
return result;
};
// the main functions
fuzzyset.get = function (value, defaultValue, minMatchScore) {
// check for value in set, returning defaultValue or null if none found
if (minMatchScore === undefined) {
minMatchScore = 0.33;
}
var result = this._get(value, minMatchScore);
if (!result && typeof defaultValue !== "undefined") {
return defaultValue;
}
return result;
};
fuzzyset._get = function (value, minMatchScore) {
var results = [];
// start with high gram size and if there are no results, go to lower gram sizes
for (
var gramSize = this.gramSizeUpper;
gramSize >= this.gramSizeLower;
--gramSize
) {
results = this.__get(value, gramSize, minMatchScore);
if (results && results.length > 0) {
return results;
}
}
return null;
};
fuzzyset.__get = function (value, gramSize, minMatchScore) {
var normalizedValue = this._normalizeStr(value),
matches = {},
gramCounts = _gramCounter(normalizedValue, gramSize),
items = this.items[gramSize],
sumOfSquareGramCounts = 0,
gram,
gramCount,
i,
index,
otherGramCount;
for (gram in gramCounts) {
gramCount = gramCounts[gram];
sumOfSquareGramCounts += Math.pow(gramCount, 2);
if (gram in this.matchDict) {
for (i = 0; i < this.matchDict[gram].length; ++i) {
index = this.matchDict[gram][i][0];
otherGramCount = this.matchDict[gram][i][1];
if (index in matches) {
matches[index] += gramCount * otherGramCount;
} else {
matches[index] = gramCount * otherGramCount;
}
}
}
}
function isEmptyObject(obj) {
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) return false;
}
return true;
}
if (isEmptyObject(matches)) {
return null;
}
var vectorNormal = Math.sqrt(sumOfSquareGramCounts),
results = [],
matchScore;
// build a results list of [score, str]
for (var matchIndex in matches) {
matchScore = matches[matchIndex];
results.push([
matchScore / (vectorNormal * items[matchIndex][0]),
items[matchIndex][1],
]);
}
var sortDescending = function (a, b) {
if (a[0] < b[0]) {
return 1;
} else if (a[0] > b[0]) {
return -1;
} else {
return 0;
}
};
results.sort(sortDescending);
if (this.useLevenshtein) {
var newResults = [],
endIndex = Math.min(50, results.length);
// truncate somewhat arbitrarily to 50
for (var i = 0; i < endIndex; ++i) {
newResults.push([
_distance(results[i][1], normalizedValue),
results[i][1],
]);
}
results = newResults;
results.sort(sortDescending);
}
newResults = [];
results.forEach(
function (scoreWordPair) {
if (scoreWordPair[0] >= minMatchScore) {
newResults.push([
scoreWordPair[0],
this.exactSet[scoreWordPair[1]],
]);
}
}.bind(this)
);
return newResults;
};
fuzzyset.add = function (value) {
var normalizedValue = this._normalizeStr(value);
if (normalizedValue in this.exactSet) {
return false;
}
var i = this.gramSizeLower;
for (i; i < this.gramSizeUpper + 1; ++i) {
this._add(value, i);
}
};
fuzzyset._add = function (value, gramSize) {
var normalizedValue = this._normalizeStr(value),
items = this.items[gramSize] || [],
index = items.length;
items.push(0);
var gramCounts = _gramCounter(normalizedValue, gramSize),
sumOfSquareGramCounts = 0,
gram,
gramCount;
for (gram in gramCounts) {
gramCount = gramCounts[gram];
sumOfSquareGramCounts += Math.pow(gramCount, 2);
if (gram in this.matchDict) {
this.matchDict[gram].push([index, gramCount]);
} else {
this.matchDict[gram] = [[index, gramCount]];
}
}
var vectorNormal = Math.sqrt(sumOfSquareGramCounts);
items[index] = [vectorNormal, normalizedValue];
this.items[gramSize] = items;
this.exactSet[normalizedValue] = value;
};
fuzzyset._normalizeStr = function (str) {
if (Object.prototype.toString.call(str) !== "[object String]")
throw "Must use a string as argument to FuzzySet functions";
return str.toLowerCase();
};
// return length of items in set
fuzzyset.length = function () {
var count = 0,
prop;
for (prop in this.exactSet) {
if (this.exactSet.hasOwnProperty(prop)) {
count += 1;
}
}
return count;
};
// return is set is empty
fuzzyset.isEmpty = function () {
for (var prop in this.exactSet) {
if (this.exactSet.hasOwnProperty(prop)) {
return false;
}
}
return true;
};
// return list of values loaded into set
fuzzyset.values = function () {
var values = [],
prop;
for (prop in this.exactSet) {
if (this.exactSet.hasOwnProperty(prop)) {
values.push(this.exactSet[prop]);
}
}
return values;
};
// initialization
var i = fuzzyset.gramSizeLower;
for (i; i < fuzzyset.gramSizeUpper + 1; ++i) {
fuzzyset.items[i] = [];
}
// add all the items to the set
for (i = 0; i < arr.length; ++i) {
fuzzyset.add(arr[i]);
}
return fuzzyset;
};
return FuzzySet;
})();
function fuzzyStringInArray(string, array, fuzzyThreshold) {
for (let i in array) {
let keyword = array[i]
if (fuzzyMatchScore(string, keyword) > fuzzyThreshold) {
// log(`Found match: ${string} and ${keyword} with score ${fuzzyMatchScore(string, keyword)}`)
return true
}
}
return false
}
function fuzzyMatchScore(needle, haystack) {
let a = FuzzySet();
a.add(haystack);
let result = a.get(needle)
if (!result) return 0
return result[0][0];
}
/**
* get keywords that are similar to the search term
* @param {string} searchTerm - the search term
* @param {array} keywords - the keywords to search through
* @returns {array} - the keywords that are similar to the search term
*/
function getSimilarKeywords(searchTerm, keywords, fuzzyMatchMoreThan, fuzzyMatchLessThan) {
let checkedKeywords = []
let similarKeywords = []
for (let i in keywords) {
let k = keywords[i]
//skip k if it's already in checkedKeywords
if (checkedKeywords.indexOf(k) > -1) {
continue;
}
checkedKeywords.push(k)
let score = fuzzyMatchScore(searchTerm, k)
if (score > fuzzyMatchMoreThan && score <= fuzzyMatchLessThan) {
let roundedScore = roundFloatToTwoDecimalPlaces(score)
similarKeywords.push(`${k} (${roundedScore})`)
}
}
return similarKeywords
}
function roundFloatToTwoDecimalPlaces(float) {
return Math.round(float * 100) / 100
}
/**
* validate the theshold setting
* @param {number} theshold - the threshold
* @returns {bool} - whether the threshold is valid
*/
function fuzzyThresholdIsValid(theshold) {
if (theshold > 1) {
return false
}
if (theshold < 0) {
return false
}
if (String(theshold) === "") {
return false
}
return true
}