Блог

Отладка кода в редакторе Apps Script

Это статья будет полезна тем, кому необходимо изменить или доработать готовый код Apps Script (в том числе и код, размещённый на этом сайте) под свои насущные потребности.

И первый вопрос:

Что делать, если изменённый скрипт перестал работать?

Разумеется, для того чтобы код снова заработал, его необходимо отлаживать или “дебажить” (от английского debug).

И прежде всего нам необходимо найти то самое место, после которого начинается проблемы и код ведёт себя совсем не так, как от него ожидают.

Стратегий здесь может быть несколько. Самая надёжная из них (но не самая быстрая) - это пошагово пройтись по всему коду, начиная с самого начала.

Для этого в окне редактора необходимо:

  • выбрать функцию, которую будем отлаживать (на рисунке это функция tmp());
  • установить breakpoint (точку останова программы) - на рисунке это фиолетовая точка слева от номера 32-й строки;
  • и нажать кнопку Debug (в заголовке над блоком кода, между кнопкой Run и названием функции отладки).

На этом рисунке программа остановилась как раз в точке останова в 32-й строке.

Там же на рисунке видно, что в правом блоке под заголовком Debugger находятся четыре кнопки:

  1. Кнопка Resume (треугольник) - нажатие на эту кнопку продолжит выполнение программы до следующей точки останова (или до конца программы, если точек останова больше нет).
  2. Кнопка Step Over (точка с дугообразной стрелкой) - нажатие обеспечивает выполнение текущей строки и переход на следующую строку.
  3. Кнопка Step In (стрелка направлена вниз к точке) - если текущая строка обычная, то будет выполнение текущей строки и переход на следующую (как и по команде Step Over). Но если текущая строка содержит функцию - переход будет осуществлён внутрь этой функции, то есть на первую строку кода внутри этой функции. (В примере на рисунке это строка 36, в которой содержится функция fillColumn. Нажатие Step In в строке 36 “заведёт” отладчик внутрь этой функции)
  4. Кнопка Step Out (стрелка вверх от точки) - текущая функция будет завершена и следующей точкой останова станет первая строки, после строки вызова текущей функции (в нашем примере это строка 37).

Ниже кнопок управления блока Debugger находится блок Variables, где показывается текущее значение переменных в каждой точке останова программы.

Кроме этого мы может вывести нужные значения в консоль с помощью команды

console.log(variable)

На снимки эти данные отображены в нижней части в блоке Execution log.

Как “отдебажить” код функции с аргументами?

Если мы хотим проверить работу функции, у которой есть аргументы, (например функции fillColumn(row, col, mask)) то лучшим решением будет сначала создать вспомогательную функцию tmp(), где

  • Сначала будут заданы значения всех аргументов функции fillColumn - переменные row, col, и mask;
  • А затем будет вызвана самом функция fillColumn(row, col, mask).

Тогда, запуск для отладки этой самой вспомогательной функции tmp(), по сути будет запуском самой функции fillColumn с аргументами.

Собственно говоря, на приведённом выше рисунке функция tmp() как раз и играет подобную вспомогательную роль для функции fillColumn(row, col, mask).

Как отлаживать код внутри функции onEdit(e)?

Системная функция onEdit(e) служит для того, чтобы перехватывать изменение/ввод данных на листах Google-таблиц, и не реагирует на стандартную обработку дебагером по breakpoint. Поэтому “отдебажить” её обычным образом у нас не получится.

Решений может быть, как минимум, два:

  1. Создать вспомогательную функцию, чтобы сначала перенести в неё всю логику функции onEdit(e) и там отладить по заранее заданным входным данным. А после отладки перенести готовый и отлаженный код обратно.
  2. Отлаживать onEdit(e) что называется “на месте”, но в качестве точек останова использовать окна, вызываемые классом Browser:

Browser.msgBox(variable);

Для удобства отладки по второму сценарию в качестве аргументов variable можно добавить значения каких-либо переменных.

Теперь для запуске второго варианта необходимо внести любые изменения на лист Google-таблицы. Это немедленно запустит функцию onEdit(e). И если до строки запуска месседж-бокса нет никаких ошибок, то мы обязательно увидим окно с именем переменной variable.

Дополнительную информация о способах отладки скриптов в редакторе Apps Script, вы сможете получить из этого видео:

Читать дальше >>

Мониторинг доступности сайта

Каждому владельцу сайта важно быть уверенным, что в данный момент его онлайн ресурс доступен и нормально работает. Ну, а если, вдруг, случится проблема, то владелец сайта должен узнать о ней раньше всех остальных.

Есть огромное число платных и условно бесплатных сервисов, которые готовы оказать услугу по круглосуточному мониторингу веб ресурсов. И, в случае их недоступности, немедленно проинформировать об этом заинтересованную сторону.

Однако, есть очень простой способ запустить эту проверку самому, в удобном для себя режиме и совершенно бесплатно.

Идея проста: используя Google Apps Script мы отправляем запрос на указанный url и анализируем код ответа. Если код ответа равен 200 - ничего не предпринимает. Ну, а если нет - отправляем на свой емейл сообщение об ошибке.

Скрипт, реализующий эту задачу, находится ниже:

function locator() {
  let sites = ['https://it4each.com/', 
               ];

  let myEmail = YourEmail;
  let subject = "Site not working!!!";
  let errors = [];
  
  // request sending and processing loop
  for (const site of sites) {
    try {
      let response = UrlFetchApp.fetch(site);
      if (response.getResponseCode() != 200 ) errors.push(site);
    } catch (e) {
      let error_messege = e.name + ': for website ' + site + '\n';
      console.error(error_messege);
      errors.push(site)
    };
  };

  // send email
  if (errors.length > 0) {
    let message = "";
    for (let error of errors) {
      message += 'Website ' + error + " doesn't working!\n";
    };
    message += '\n' + 'Remaining Daily Quota: ' + MailApp.getRemainingDailyQuota();

    MailApp.sendEmail(myEmail, subject, message)
  };
}

За работу сайтов наблюдает функция locator(). Предварительно, в эту функцию должны быть переданы следующие исходные данные:

  • Список сайтов sites;
  • Адрес почты, куда следует отправлять сообщение об ошибке myEmail;
  • Тему (заголовок) электронного сообщения subject.

Далее идёт цикл отправки и обработки запросов. Это делается с помощью стандартного метода fetch(url) класса UrlFetchApp.

Если ресурс в принципе доступен, но его код ответа изменился и более не равен 200, то имя проблемного ресурса добавляется в список ошибок errors в той же строке.

Но если ресурс вообще недоступен, то UrlFetchApp.fetch(site) даст ошибки, которая может привести к остановке программы. Чтобы этого не произошло, вариант подобной ошибки мы обработаем через try - catch(e). И добавление "битого" сайта произойдёт на этот раз в блоке catch.

Обработка результата будет проиcходить ниже, в блоке send email.

Если список ошибок не пустой, то в цикле будет сформировано сообщение message, где будут перечислены все неработающие сайты. Дополнительно будет добавлена информация о том, сколько подобных сообщений ещё можно создать на сегодня, чтобы не превысить квоту: MailApp.getRemainingDailyQuota().

Скрипт готов. Но для того, чтобы осуществлять полноценный мониторинг, необходимо запускать этот скрипт регулярно и круглосуточно. Помочь нам в этом сможет установка триггера.

Узнать о том, как создать и настроить триггер, а также получить дополнительную информация о работе данного скрипта, вы сможете из этого видео:

Читать дальше >>

Бесконечные Связанные Списки: Личный Финансовый План (пример #1)

Данная статья является продолжением статьи Бесконечные Зависимые Выпадающие Списки в Google Sheets (часть 3).

Здесь рассмотрен пример практического применения Бесконечных связанных списков при составлении Личного Финансового Плана.

Помимо добавления нескольких удобных для пользователя функций (автоматическая вставка даты внесения платежа и формулы для ведения баланса приходов-расходов) скрипт, приведённый содержит целый ряд существенных доработок и улучшений:

// global variable
const excludedSheets = ['Summary', 'Category'];
const dataSheet = 'Category';

var colShift = 3;
var rowMin = 4;

function onEdit(e) {

  let row = e.range.getRow();
  let col = e.range.getColumn();
  let sheetName = e.source.getActiveSheet().getName();
  if (excludedSheets.includes(sheetName)) return;
  let name = e.value;
  let oldName = e.oldValue;
  let sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  let mask = JSON.stringify(sh.getRange(row, colShift+1, 1, col-colShift).getValues()[0]);
  let colMax = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(dataSheet).getLastColumn();
  
  if(name !== oldName && row >= rowMin && col < colMax+colShift) {
    insertDateFormulasAndDataValidation(row, col, sheetName)
    fillColumn(row, col, mask, sheetName);
  }
}

function insertDateFormulasAndDataValidation(row, col, sheetName) {

  if (col == colShift+1) {
    let sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
    let dateValue = sh.getRange(row, 2).getValue();
    if (dateValue == '') {
      let today = new Date();
      let timeZone = Session.getScriptTimeZone();
      let todayStr = Utilities.formatDate(today, timeZone, 'dd.MM.yyyy');
      sh.getRange(row, 2).setValue(todayStr);
    };
    let formulaStr = '=I' + (row-1) + '+H' + row + '-C' + row; 
    sh.getRange(row, 9).setFormula(formulaStr);
    let conN = String.fromCharCode(65+colShift) 
    let adress = sheetName + '!' + conN + (row+1) + ':' + conN + (row+3);
    sh.getRange(adress).setDataValidation(SpreadsheetApp.newDataValidation()
      .setAllowInvalid(false)
      .requireValueInRange(sh.getRange(dataSheet+'!$A$2:$A'), true)
      .build());    
  }
}

function tmp() {
  insertDateFormulasAndDataValidation(7, 4, 'Transactions');
}

function onOpen() {
  let ui = SpreadsheetApp.getUi();
  // Or DocumentApp or FormApp.
  ui.createMenu('Custom Menu')
      .addItem('Create sheets', 'createSheets')
      .addToUi();
}

function fillColumn(row, col, mask, sheetName) {

  let col_data = col - colShift;

// clear dataVal and Value
  let sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  let colMax = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(dataSheet).getLastColumn();
  sh.getRange(row, col + 1, 1, colMax-col_data).clearDataValidations().clearContent();
 
// find date
  let sd = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(dataSheet);
  let arrData = sd.getDataRange().getValues();
  let arrData_2 = [];
  
  let iMax = arrData.length - 1;
  for(let i=1; i<=iMax; i++) {
    if(JSON.stringify(arrData[i].slice(0, col_data)) == mask) {
      arrData_2.push(arrData[i].slice(0, col_data+1));
    }
  }
  arrData_2 = arrData_2.map(item => item.pop())
  let uniqArrDate_2 = arrData_2.filter(uniqValues); 

// add dataVal
  col++;
  sh.getRange(row, col).setDataValidation(SpreadsheetApp.newDataValidation()
  .setAllowInvalid(false)
  .requireValueInList(uniqArrDate_2, true)
  .build());
}

function uniqValues(item, index, arr) {
  return arr.indexOf(item) === index;
}

Дополнительную информацию вы можете получить из этого видео:

Читать дальше >>

Бесконечные Зависимые Выпадающие Списки в Google Sheets (часть 3)

Данная статья является продолжением статьи Бесконечные Зависимые Выпадающие Списки в Google Sheets (часть 2).

Здесь описаны изменения изменения и дополнения кода, а именно:

  • Автоматическое создание листа Номе ;
  • Автоматическое добавление всех формул на листе Номе;
  • При редактировании созданной ранее строки зависимых (связанных списков), автоматически удаляются значения и формулы справа от редактируемой ячейки.
  • Возможность сдвига таблицы на листе Номе вправо на произвольное число столбцов
  • 01.10.2021 Добавлена проверка наличия листов Home и Data в рабочем файле (книге)

var colShift = 0;

function onEdit(e) {

  let row = e.range.getRow();
  let col = e.range.getColumn();
  let sheetName = e.source.getActiveSheet().getName();
  let name = e.value;
  let oldName = e.oldValue;
  let sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  let mask = JSON.stringify(sh.getRange(row, colShift+1, 1, col-colShift).getValues()[0]);
  if (!doesSheetExist('Home', true)) return;
  if (!doesSheetExist('Data', true)) return;

  let colMax = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Data").getLastColumn();
  
  if(sheetName === "Home" && name !== oldName && col < colMax+colShift) {
    fillColumn(row, col, mask);
  }
}

function onOpen() {
  let ui = SpreadsheetApp.getUi();
  // Or DocumentApp or FormApp.
  ui.createMenu('Custom Menu')
      .addItem('Create sheets', 'createSheets')
      .addToUi();
}

function fillColumn(row, col, mask) {

  let col_data = col - colShift;

// clear dataVal and Value
  let sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Home');
  let colMax = sh.getLastColumn();
  sh.getRange(row, col + 1, 1, colMax).clearDataValidations().clearContent();
 
// find date
  let sd = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Data");
  let arrData = sd.getDataRange().getValues();
  let arrData_2 = [];
  
  let iMax = arrData.length - 1;
  for(let i=1; i<=iMax; i++) {
    if(JSON.stringify(arrData[i].slice(0, col_data)) == mask) {
      arrData_2.push(arrData[i].slice(0, col_data+1));
    }
  }
  arrData_2 = arrData_2.map(item => item.pop())
  let uniqArrDate_2 = arrData_2.filter(uniqValues); 

// add dataVal
  col++;
  sh.getRange(row, col).setDataValidation(SpreadsheetApp.newDataValidation()
  .setAllowInvalid(false)
  .requireValueInList(uniqArrDate_2, true)
  .build());
}

function createSheets() {

// is exist Home?
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sd;
  if (doesSheetExist('Data', true)) {
    sd = ss.getSheetByName('Data');
  } else {
    return;
  }
  
  
// create if not exist
  if(!ss.getSheetByName('Home')) {
    ss.insertSheet('Home', 0);
// create Data Val
    var sh = ss.getSheetByName('Home');
    sh.getRange('Home!A2:A20').setDataValidation(SpreadsheetApp.newDataValidation()
      .setAllowInvalid(false)
      .requireValueInRange(sh.getRange('Data!$A$2:$A'), true)
      .build());
    sh.getRange(1, 1, 1, 10).setValues(sd.getRange(1, 1, 1, 10).getValues()).setFontWeight('bold');

  };

}

function uniqValues(item, index, arr) {
  return arr.indexOf(item) === index;
}


function doesSheetExist(sheetName, needAlert) {
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let allSheets = ss.getSheets().map(sheet => sheet.getName())
  if (allSheets.includes(sheetName)) return true;
  if (needAlert) Browser.msgBox(`Error! Could not find sheet "${sheetName}"!`);
  
  return false;
}

В этой версии программы сохранена возможность по нажатию всего лишь одной(!) кнопки из пользовательского меню автоматически создавать все листы файла, необходимые для работы скрипта (включая форматирование и валидацию данных).

Всё что для этого нужно:

  1. cоздать лист с именем "Data"
  2. скопировать в него данные, необходимые для связанных (зависимых) списков
  3. скопировать и встваить в файл этот скрипт
  4. обновить страницу
  5. в появившемся пользовательском меню выбрать: Create sheets

Дополнительную информацию вы можете получить из этого видео:

ВНИМАНИЕ!
У этой статьи есть продолжение: Бесконечные Связанные Списки: Личный Финансовый План (пример #1)

Читать дальше >>

Не работает парсинг сайта с помощью Apps Script. Почему?

Способ парсинга (скрейпинга), рассмотренный в статье Парсинг (Скрапинг) с помощью Google Apps Script, невероятно прост и удобен. Здесь не нужно ни сложных программ, ни высокой квалификации или специальных знаний, ни большого опыта. Весь код пишется прямо в редакторе Google-таблиц, и затем там же размещается и запускается.

Плюс ко всему этому, скрипт можно запускать по расписанию в любое время в режиме 24 / 7, а полученный результат - прямо тут же обработать и, если надо, отправить на электронную почту или создать на Гугл-диске новый файл с полученными и обработанными данными.

Однако, и здесь могут быть свои проблемы. Прежде всего, этот скрипт не работает с сайтами (точнее, со страницами), где необходима предварительная регистрация. Поскольку, для регистрации на сайте необходимо создать сессию и разместиь её код в специальных файлах - cookies. Однако, стандартная функция UrlFetchApp, используемая в Apps Script для доступа к веб-сайту, не позволяет работать c cookies.

Есть и другая проблема, которая в последее время проявляется всё чаще и чаще. Это - нежелание владельцев сайтов делиться информацией с ботами. Поэтому, нередко можно видеть картину, когда html-код, выдаваемый сервером по запросу скрипта, существенно отличается от кода, который выдаётся браузеру.

Собственно, именно для того, чтобы быстро узнать, можно ли парсить (скрейпить) нужный нам сайт или нет, и была написана эта программа:

function onOpen() {
  var ui = SpreadsheetApp.getUi();
  // Or DocumentApp or FormApp.
  ui.createMenu('Custom Menu')
      .addItem('Check', 'fastAnalyzer')
      .addSeparator()
      .addItem('Create Sheeet CHECK', 'createSheet')
      .addToUi();
}


function fastAnalyzer() {
  const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Check');
  const urlDomain = ss.getRange(2, 1).getValue();
  
  let url = urlDomain;
  let count = 0;
  let response = UrlFetchApp.fetch(url);
  let lll = response.getContentText().length;
  ss.getRange(2, 2).setValue(response.getResponseCode());
  ss.getRange(2, 3).setValue(lll);
  let html = response.getContentText();
  let whatLookingFor = ss.getRange(3, 2).getValue();
  let pos = html.indexOf(whatLookingFor);
  ss.getRange(3, 3).setValue(pos);

  ss.getRange(4, 1, ss.getLastRow(), 2).clearContent();
  let totalNumber = html.length
  let i = 0;

  // Check defoult value of delta - number of symbols in 1 row
  // If the cell is empty, print the defole value = 5000
  let delta = Number.parseInt(ss.getRange(2, 4).getValue());
  if (isNaN(delta)) {
    delta = 5000
    // Print defoult value of number of symbols in 1 row
    ss.getRange(2, 4).setValue(delta);
  };

  let iStart = 0;
  while (true) {
    let iEnd = iStart + delta;
    if (iEnd > totalNumber) iEnd = totalNumber; 
    ss.getRange(4 + i, 1).setValue("Символы от " + iStart + " до " + iEnd);
    ss.getRange(4 + i, 2).setValue(html.slice(iStart,iEnd));
    // let currentRow = (4+i) + ":" + (4+i)
    // ss.getRange(3, 3).setValue(currentRow);
    // ss.getRange(currentRow).activate();
    // ss.setRowHeight(4+i, 200);
    i++;
    if (iEnd - iStart < delta) break;
    iStart = iEnd;

  };
}

function createSheet() {

// is exist Check sheet?
  let s = SpreadsheetApp.getActiveSpreadsheet();
  
// create if not exist
  if(!s.getSheetByName('Check')) {
    s.insertSheet('Check', 0);
    ssc = s.getSheetByName('Check');

    ssc.getRange(1, 1).setValue("Url сайта");
    ssc.getRange(1, 2).setValue("Код ответа сайта");
    ssc.getRange(1, 3).setValue("Число символов на выбранной странице");
    ssc.getRange(1, 4).setValue("Изменить вывод числа знаков в однй строке");
    ssc.getRange(2, 1).setBackground('ACCENT3');  // yellow background cell
    ssc.getRange(3, 1).setValue("Какой элемент кода ищем?");
    ssc.getRange(3, 2).setBackground('ACCENT3');  // yellow background cell
  };
}

Итак, всё, что вам нужна для работы с этой программой - это

  • создать новый файл Google Spreadsheet
  • открыть в этом файле редактор кода
  • полностью скопировать код из этой статьи в редактора кода только что созданного файла (и не забыть его сохранить!)
  • обновить страницу электронной таблицы и выбрать пункт пользовательского меню: Create Sheet CHECK (Создать Листь ПРОВЕРКИ)

Программа готова к работе! Всё, что теперь нужно, это - добавить на страницу Гугл-таблицы url сайта, элемент кода, который вы хотите найти на этой странице, и запустить скрипт. Ответ будет готов через секунду!

Более подробно о работе скрипта рассказано в этом видео:

Читать дальше >>

Парсинг (Скрапинг) с помощью Google Apps Script

Скрипт, представленный ниже, позволяют автоматически находить и выгружать информацию об объявлениях на одной из бирж фриланса.

Имя домена берётся со страницы Google Spread Sheets. Туда же выгружаются результаты поиска.

В качестве управляющего скрипта используется функция scraper(), которая содержит два цикла. В первом цикле (for-цикл) идёт обращение к веб-страницам сайта, и их сохранение в переменной html. Во втором цикле (while-цикл) идёт последовательная обработка этой переменной с помощью трёх вспомогательных функций:

  • Функция getBlock находит часть html-кода (блок кода) внутри тега (обычно по уникальному значению атрибутов этого тега), и возвращает этот блок в виде строкового значения (без самого тега!);
  • Функция deleteBlock наоборот, удаляет найденный фрагмент html-кода внутри блока и также возвращает оставшуюся часть этого блока в виде строкового значения.
  • В отличии от первых двух функций, функция getOpenTag не удаляет найденный тег, а возвращает его в виде строкового знечения. Правде, не весь тег, а только первую (открывающую часть) этого тега.

function scraper() {
  
  const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
  const urlDomain = ss.getRange(1, 1).getValue();
  
  let url = urlDomain;
  let count = 0;
  for (let page = 1; page < 5; page++) {
    url = urlDomain + page + '/';
    if (page == 1) url = urlDomain;
    let response = UrlFetchApp.fetch(url);
    ss.getRange(2, 1).setValue(response.getResponseCode());
    let html = response.getContentText();
    let p = 0;
    while (true) {
    
      let out = getBlock(html, 'div', html.indexOf('class="JobSearchCard-primary"', p));
      let block = out[0];
      p = out[1] + 1;
      if (p == 0) break;
      
      let title1 = getBlock(block, 'div', 0)[0];
      let title = getBlock(title1, 'a', 0)[0];
      
      let link = getOpenTag(title1, 'a', 0);
      link = getAttrName(link, 'href', 0)
      let formula = '=HYPERLINK("https://www.freelancer.com' +link + '", "' + title + '")';
      ss.getRange(3 + 3 * count, 2).setValue(formula);
      
      let price = getBlock(block, 'div', block.indexOf('class="JobSearchCard-primary-price'))[0];
      if (price.includes('span')) price =  deleteBlock(price, 'span', price.indexOf('span'));
      ss.getRange(3 + 3 * count + 1, 2).setValue(price).setHorizontalAlignment('right');
      
      let description = getBlock(block, 'p', block.indexOf('class="JobSearchCard-primary-description"'))[0];
      ss.getRange(3 + 3 * count, 1, 3).mergeVertically().setValue(description)
          .setBorder(true, true, true, true, null, null, '#000000', SpreadsheetApp.BorderStyle.SOLID)
          .setVerticalAlignment('middle')
          .setWrapStrategy(SpreadsheetApp.WrapStrategy.WRAP);
  
      ss.getRange(3 + 3 * count, 2, 3).setBorder(true, true, true, true, null, null, '#000000', SpreadsheetApp.BorderStyle.SOLID);
      
      let cat = getBlock(block, 'div', block.indexOf('class="JobSearchCard-primary-tags"'))[0];
      cat = cat.split('</a>').map(item => item.split('>')[1]);
      cat.pop();
      cat = cat.join(', ');
      ss.getRange(3 + 3 * count + 2, 2).setValue(cat);
    
      count++;
    
    }; 
  };
}

function getAttrName(html, attr, i) {
  let idxStart = html.indexOf(attr +'=' , i);
  if (idxStart == -1) return "Can't to find attr " + attr + ' !';
  idxStart = html.indexOf('"' , idxStart) + 1;
  let idxEnd = html.indexOf('"' , idxStart);
  return html.slice(idxStart,idxEnd).trim();
}

function getOpenTag(html, tag, idxStart) {
  let openTag = '<' + tag;
  let lenOpenTag = openTag.length;
  // where we are?
  if (html.slice(idxStart, idxStart + lenOpenTag) != openTag) {
    idxStart = html.lastIndexOf(openTag, idxStart);
    if (idxStart == -1) return "Can't to find openTag " + openTag + ' !';
  };
  // begin loop after openTag
  let idxEnd = html.indexOf('>', idxStart) + 1;
  if (idxStart == -1) return "Can't to find closing bracket '>' for openTag!";
  return html.slice(idxStart,idxEnd).trim();
}

function deleteBlock(html, tag, idxStart) { // delete opening & closing tag and info between them
  let openTag = '<' + tag;
  let lenOpenTag = openTag.length;
  let closeTag = '</' + tag + '>';
  let lenCloseTag = closeTag.length;
  let countCloseTags = 0;
  let iMax = html.length;
  let idxEnd = 0;
  // where we are?
  if (html.slice(idxStart, idxStart + lenOpenTag) != openTag) {
    idxStart = html.lastIndexOf(openTag, idxStart);
    if (idxStart == -1) return ["Can't to find openTag " + openTag + ' !', -1];
  };
  // begin loop after openTag
  let i = html.indexOf('>') + 1;
  
  while (i <= iMax) {
    i++;
    if (i === iMax) {
      return ['Could not find closing tag for ' + tag, -1];
    };
    let carrentValue = html[i];
    if (html[i] === '<'){
      let closingTag = html.slice(i, i + lenCloseTag);
      let openingTag = html.slice(i, i + lenOpenTag);
      if (html.slice(i, i + lenCloseTag) === closeTag) {
        if (countCloseTags === 0) {
          idxEnd = i + lenCloseTag;
          break;
        } else {
          countCloseTags -= 1;
        };
      } else if (html.slice(i, i + lenOpenTag) === openTag) {
        countCloseTags += 1;
      };
    };
  };
  return (html.slice(0, idxStart) + html.slice(idxEnd, iMax)).trim();
}

function getBlock(html, tag, idxStart) {  // <tag .... > Block </tag>
  let openTag = '<' + tag;
  let lenOpenTag = openTag.length;
  let closeTag = '</' + tag + '>';
  let lenCloseTag = closeTag.length;
  let countCloseTags = 0;
  let iMax = html.length;
  let idxEnd = 0;
  // where we are?
  if (html.slice(idxStart, idxStart + lenOpenTag) != openTag) {
    idxStart = html.lastIndexOf(openTag, idxStart);
    if (idxStart == -1) return ["Can't to find openTag " + openTag + ' !', -1];
  };
  // change start - will start after openTag!
  idxStart = html.indexOf('>', idxStart) + 1;
  let i = idxStart;
  
  while (i <= iMax) {
    i++;
    if (i === iMax) {
      return ['Could not find closing tag for ' + tag, -1];
    };
    let carrentValue = html[i];
    if (html[i] === '<'){
      let closingTag = html.slice(i, i + lenCloseTag);
      let openingTag = html.slice(i, i + lenOpenTag);
      if (html.slice(i, i + lenCloseTag) === closeTag) {
        if (countCloseTags === 0) {
          idxEnd = i - 1;
          break;
        } else {
          countCloseTags -= 1;
        };
      } else if (html.slice(i, i + lenOpenTag) === openTag) {
        countCloseTags += 1;
      };
    };
  };
  return [html.slice(idxStart,idxEnd + 1).trim(), idxEnd];
}

Более продробную информацию вы сможете найти в этом видео:

Читать дальше >>

Массивы Google Apps Script и JavaScript. Методы поиска

В примерах, приведённых ниже, подробно рассматриваются JavaScript и Apps Script методы поиска элементов массивов : find(), findIndex(), indexOf(), lastIndexOf(), includes(), every() и some().

Методы findIndex(), indexOf() и lastIndexOf() позволяют найти индекс массива, удовлетворяющий заданному условию, а метод find() позволяет найти его значение.

Метод includes() удобен для использования в операторе if , поскольку возвращает true, если элемент найден, и false, если нет.

Метод every() проверяет соответствие ВСЕХ элементов массива указанному в callback функции условию, а метод some() проверяет, удовлетворяет ли этому условию ХОТЯ БЫ ОДИН элемент массива.

const arr = [1, 2, 3, 4, 5, 3];
  
// find() returns the VALUE of the FIRST item satisfies condition of the callback function
// OR return <underfined>

  let valueGteater3 = arr.find(item => item > 3);
  console.log(valueGteater3); // 4
  
  let valueGteater5 = arr.find(item => item > 5);
  console.log(valueGteater5); // undefined

// findIndex() returns the INDEX of the FIRST item satisfies condition of the callback function
// OR return <-1>

  let indexItemGteater3 = arr.findIndex(item => item > 3);
  console.log(valueGteater3); // 3
  
  let indexItemGteater5 = arr.findIndex(item => item > 5);
  console.log(valueGteater5); // -1
  
// variant for not first item  
  
  let notFirstItem = arr.find((item, index) => {
    if (item > 3 && index > 3) return true;
  });
  console.log(notFirstItem); // 5
  
// indexOf() returns the INDEX of the FIRST item is equal the specified value
// OR return <-1>

  let valueEqual3 = arr.indexOf(3);
  console.log(valueEqual3); // 2
  
  let valueEqual3Next = arr.indexOf(3, 3);
  console.log(valueEqual3Next); // 5
 
  let valueEqual1 = arr.indexOf(1, 3);
  console.log(valueEqual1); // -1
  
// lastIndexOf() returns the INDEX of the LAST item is equal the specified value
// OR return <-1>

  let valueEqual3Last = arr.lastIndexOf(3);
  console.log(valueEqual3Last); // 5
  
  let valueEqual3NextLast = arr.lastIndexOf(3, 4);
  console.log(valueEqual3NextLast); // 2
 
  let valueEqual5Last = arr.lastIndexOf(5, 3);
  console.log(valueEqual5Last); // -1
  
// const arr = [1, 2, 3, 4, 5, 3];
// includes() returns TRUE is element is icluded in the array, and FALSE is not

  let isIncludes3 = arr.includes(3);
  console.log(isIncludes3); // true
  
  let isIncludes6 = arr.includes(6);
  console.log(isIncludes6); // false
  
// every() returns TRUE if ALL elements satisfy the condition, and FALSE is not

  let allPositive = arr.every(item => item > 0);
  console.log(allPositive); // true
  
  let moreThen1 = arr.every(item => item > 1);
  console.log(moreThen1); // false

// some() returns TRUE if AT LEAST ONE element satisfy the condition, and FALSE is not

  let moreThen4 = arr.some(item => item > 4);
  console.log(moreThen4); // true
  
  let moreThen5 = arr.some(item => item > 5);
  console.log(moreThen5); // false

Более продробную информацию вы сможете найти в этом видео:

Читать дальше >>

Массивы Google Apps Script и JavaScript. Метод .reduce()

Краткое описание метода

Метод .reduce() как и большинство других методов, использующих callback функцию, представляет собой цикл for, внутри которого callback функция последовательно обрабатывает все элементы массива. Однако, следует добавить, что что это единственный метод, который используе ещё один параметр - accumulator (аккумулятор), блягодаря чему метод .reduce() часто используется для вычислений суммы массива.

Метод .reduce() может передавать в callback функцию 4 параметра:

  1. текщее значение суммы элемента массива accumulator
  2. текщее значение элемента массива item
  3. необязательный параметр index - индекс текущего элемента
  4. необязательный и редко используемый параметр array

Этот метод может быть использован и для более сложных вычислений. Например, ниже приводится скрипт, который создаёт массив индексов элементов массива arr, представлящих собой нечётные числа.

const arr = [1, 2, 3, 4, 5];
  
// sum with for loop
  let sumArr = 0;
  for (let i = 0; i < arr.length; i++) {
    sumArr += arr[i];
  };
  console.log(sumArr);
  
// sum with forEach method
  let sumArr_1 = 0;
  arr.forEach(item => sumArr_1 += item);
  console.log(sumArr_1);
  
// sum with reduce method
  let sumArr_2 = arr.reduce((acc, item) => acc + item, 0);
  console.log(sumArr_2);
  
// with initial value
  sumArr_2 = arr.reduce((acc, item, index, array) => {
    console.log(`acc = ${acc}, item = ${item}, index = ${index}`);
    return acc + item;
  }, 0);
  console.log(sumArr_2);
  
// without initial value
  sumArr_2 = arr.reduce((acc, item, index, array) => {
    console.log(`acc = ${acc}, item = ${item}, index = ${index}`);
    return acc + item;
  });
  console.log(sumArr_2);
  
// find indexes items that are odd numbers  
  let newArr = arr.reduce((acc, item, index) => {
    if (item % 2 !== 0) acc.push(index);
    return acc;
  }, []);
  console.log(newArr); // [ 0, 2, 4 ]
  
 // reduceRight
  sumArr_2 = arr.reduceRight((acc, item, index, array) => {
    console.log(`acc = ${acc}, item = ${item}, index = ${index}`);
    return acc + item;
  });
  console.log(sumArr_2);

Дополнительную информацию вы можете найти в этом видео:

Читать дальше >>

Массивы Google Apps Script и JavaScript. Метод .forEach()

Краткое описание метода

Метод .forEach() , по сути, представляет собой цикл for, внутри которого callback функция последовательно обрабатывает все элементы массива.

Метод .forEach() может передавать в callback функцию три параметра:

  1. текщее значение элемента массива item
  2. необязательный параметр index - индекс текущего элемента
  3. необязательный и редко используемый параметр array

Это, пожалуй, единственный метод, который ничего не возвращает. Поэтому используется для внешних обработок, например, для вывода элементов массива на печать.

В частности, в последнем примере с помощью методов .forEach() и .includes() создаётся новый массив result, который содержит в себе общие элементы массивов arr1 и arr2, то есть является результатом пересечения этих массивов.

const arr = [1, 2, 3, 4, 5];
  
  for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
  };

// variant #1
  arr.forEach(function(item) {
    console.log(item);
  });

// variant #2
  arr.forEach(item => console.log(item));
  
  console.log('------------------------------------');
  
// callback function parameters
  arr.forEach((item, idx, array) => console.log(item, idx, array));

  console.log('------------------------------------');

// get intersection of arrays
  let arr1 = [1, 3, 5];
  let arr2 = [1, 2, 3, 4, 5];
  let result = [];

  let res = arr1.forEach(item => {
    if (arr2.includes(item)) return result.push(item);
  });

  console.log(result); // [ 1, 3, 5 ]
  console.log(res); // undefined

Дополнительную информацию вы можете найти в этом видео:

Читать дальше >>

Массивы Google Apps Script и JavaScript. Метод .filter()

Краткое описание метода

Метод .filter() последовательно сравнивает все элементы массива с условием фильтра, указанным в callback функции (функции обратного вызова). То есть метод, по сути, представляет собой цикл for, внутри которого callback функция последовательно обрабатывает все элементы массива.

Метод .filter() может передавать в callback функцию три параметра:

  1. текщее значение элемента массива item
  2. необязательный параметр index - индекс текущего элемента
  3. необязательный и редко используемый параметр array

Для того, чтобы элемент, удовлетворяющий условию фильтра, был выбран, callback функция должна вернуть значение true.

Все элементы, для которых callback функция вернула знечение true, собираются в отдельный массив, который может быть сохранён в новом массива (в примерах ниже это массив result2).

В трёх вариантов примера, представленных ниже (examples #1, #2, #3) рассмотрено одно и то же условие фильтра - отобрать все элементы массива со значением > 3.

В последнем примере (наиболее оптимальным с точки зрения краткости кода) логическое выражение, вместо оператора if , поставлено непосредственно в return . Поскольку, как известно, логическое выражение может принимать только 2 значения: true или false.

const arr = [1, 2, 9, 4, 1, 6, 5];
    
    let result = arr.filter((item, idx, arr) => console.log(item, idx, arr));
//  [20-06-15 14:53:21:082 EEST] 1 0 [ 1, 2, 9, 4, 1, 6, 5 ]
//  [20-06-15 14:53:21:084 EEST] 2 1 [ 1, 2, 9, 4, 1, 6, 5 ]
//  [20-06-15 14:53:21:085 EEST] 9 2 [ 1, 2, 9, 4, 1, 6, 5 ]
//  [20-06-15 14:53:21:086 EEST] 4 3 [ 1, 2, 9, 4, 1, 6, 5 ]
//  [20-06-15 14:53:21:087 EEST] 1 4 [ 1, 2, 9, 4, 1, 6, 5 ]
//  [20-06-15 14:53:21:089 EEST] 6 5 [ 1, 2, 9, 4, 1, 6, 5 ]
//  [20-06-15 14:53:21:090 EEST] 5 6 [ 1, 2, 9, 4, 1, 6, 5 ]
    
    console.log(result);
//  [20-06-15 14:53:21:091 EEST] []  

// variant #1
  let result2 = arr.filter(item => {
      if (item > 3) {
        return true;
      } else {
        return false;
      };
    });
    
// variant #2
    result2 = arr.filter(item => {
      return item > 3;
    });


// variant #3
    result2 = arr.filter(item => item > 3);
    
    console.log(result2); // [ 9, 4, 6, 5 ]
    
    var sf = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
    let data = sf.getDataRange().getValues().slice(1);
    
    console.log(data);
//  [ [ '', false, 'apples', 5 ],
//    [ '', true, 'carrots', 12 ],
//    [ '', false, 'grapes', 4 ],
//    [ '', false, 'plums', 3 ],
//    [ '', true, 'strawberry', 9 ],
//    [ '', false, 'perches', 4 ],
//    [ '', false, 'bananas', 1 ] ]   

    let newData = data.filter(item => item[1]);

    console.log(newData); 
// [ [ '', true, 'carrots', 12 ], 
//   [ '', true, 'strawberry', 9 ] ]

И ещё одни полезный скрипт, который не вошёл в это видео - скрипт, который фильтрует ТОЛЬКО уникальные значения.

Идея очень проста: для каждого элемента массива находится его индекс и сравнивается с текущим индексом значения этого элемента в массиве. Если элемент не встретился ранее, то значения индексов будут равны, и функция uniqValue вернёт true .

В первом примере, для лучшего понимания алгоритма, функция uniqValue написана отдельно. Во втором - непосредственно "встроена" в метод .filter

let arrayNotUniq = [1, 5, 9, 5];

//variant #1

function uniqValues(item, index, arr) {
  return arr.indexOf(item) === index;
}

let arrayUniq = arrayNotUniq.filter(uniqValues)                        // [1, 5, 9]



//variant #2

  let arrayUniq = arrayNotUniq.filter((item, index, arr) => {
    return arr.indexOf(item) === index;
  });                                                                                                     // [1, 5, 9]

Дополнительную информацию вы можете найти в этом видео:

Читать дальше >>

Список тэгов

    Apps Script      Arrays Java Script      asynchronous code      asyncio      coroutine      Django      Dropdown List      Drop Shipping      Exceptions      GitHub      Google API      Google Apps Script      Google Docs      Google Drive      Google Sheets      multiprocessing      Parsing      Python      regex      Scraping      ssh      Test Driven Development (TDD)      threading      website monitoring      zip