Nghịch cùng Google Apps Script (P1) : Google Spreadsheet ft. Google Calendar

I. Mào đầu câu chuyện

Nhớ hồi còn đi học, chả phải để tâm, lo lắng đến điều gì, chỉ có đến dịp Tết là mình ngồi đếm lịch âm để biết số ngày nghỉ còn lại. Mỗi năm mỗi tuổi, cái tuổi nó đuổi xuân đi, giờ đây khi đã lập gia đình, có nhiều việc phải lo lắng suy nghĩ, mình dần dần để tâm đến lịch âm, đến ngày xấu, ngày đẹp nhiều hơn vì các cụ nhà ta từ xưa đã có câu "có thờ có thiêng, có kiêng có lành". Ngẫm ra thì không phải là không có cái lý của nó.

Thế là mình đi tìm một ứng dụng lịch vạn niên cài vào điện thoại. Mình có thể xem ngày xấu, ngày đẹp qua ứng dụng, tuy nhiên các ứng dụng mình thử qua đều không có tính năng nhắc nhở sắp đến những ngày lễ âm lịch như mồng một, rằm, lễ ông công, ông táo để thắp hương. Hơn nữa, những sự kiện ngày âm lịch này, mình muốn hiển thị chúng luôn trên lịch trình hàng ngày để tiện theo dõi mà không phải sử dụng thêm một ứng dụng nữa.

II. Giải pháp

Mình đang dùng ứng dụng Google Calendar để quản lý lịch trình hằng ngày. Google Calendar cũng có cung cấp sẵn một số bộ lịch mặc định như Ngày nghỉ lễ của Việt Nam, tuy nhiên mình lại không tìm thấy bộ lịch nào đáp ứng được nhu cầu của mình. Do vậy, mình đã quyết định tự tạo ra 4 bộ lịch "made by Enso" như sau:

  • Ngày Âm Lịch: mỗi ngày dương lịch được thêm một sự kiện (full day) có tiêu đề chính là ngày âm lịch của ngày đó. Ví dụ ngày 20/7/2017 có một sự kiện với tiêu đề 27/6/2017. Thực ra Google Calendar đã có sẵn tính năng hiển thị lịch âm. Chúng ta có thể kích hoạt bằng cách vào Calendar Settings >> General >> Alternate Calendar >> Chinese calendar (Simplified hoặc Traditional). Tuy nhiên có thể Google mặc định chỉ người Trung Quốc mới dùng lịch này nên hiển thị ngày bằng hán tự @_@.

  • Ngày Lễ Âm Lịch: những ngày dương lịch tương ứng với các ngày lễ âm lịch trong năm như mồng một, ngày rằm, tết nguyên đán, phật đản, ông công ông táo, giỗ tổ Hùng Vương,… thì sẽ có một sự kiện (full day) với tiêu đề là tên các ngày lễ âm lịch đó.

  • Tam Nương – Ly sào: những ngày dương lịch tương ứng với các ngày Tam nương hay Ly sào (ngày xấu cần tránh) trong âm lịch thì sẽ có một sự kiện (full day) với tiêu đề Tam nương hoặc Ly sào. Nếu có hứng thú các bạn có thể tìm trên Google để hiểu rõ hơn về ngày Tam Nương hay Ly sào. Đây là blog kỹ thuật nên mình xin phép không dẫn link hay nói sâu hơn :)

  • Xem Ngày Đẹp: mỗi ngày dương lịch được thêm một sự kiện (full day) có tiêu đề là Xem ngày, và mục mô tả (description) là thông tin về ngày đó (sao xấu, sao tốt, giờ xuất hành,…).

Câu hỏi bây giờ là làm thế nào để tạo ra các sự kiện cho 4 bộ lịch này. Đây chính là lúc Google App Script (GAS) thể hiện được khả năng thần thánh của mình. Chúng ta hãy cùng bỏ thời gian tìm hiểu một chút về GAS.

III. GAS là gì?

GAS là ngôn ngữ scripting dựa trên JavaScript cho phép người dùng thao tác với các sản phẩm của gói dịch vụ G Suite như Docs, Sheets hay Forms. Một số việc chúng ta có thể làm được với GAS như:

  • Thêm menu, dialog hay sidebar vào Google Doc, Sheet và Form.

  • Viết function cho Google Spreadsheet.

  • Tạo các ứng dụng web độc lập hay ứng dụng web nhúng trong Google Sites.

  • Giao tiếp với các dịch vụ khác của Google như AdSense, Analytics, Calendar, Drive, Gmail và Maps.

  • Xây dựng các add-on mở rộng cho Google Docs, Sheets, Forms và đưa chúng lên Add-on store.

  • Chuyển một ứng dụng Android thành add-on Android có thể trao đổi dữ liệu với Google Doc hay Google Spreadsheet của người dùng trên thiết bị di động.

Những điều GAS làm được quả thật rất thú vị. Vậy bạn cần những gì để có thể sử dụng được GAS. Rất đơn giản, bạn chỉ cần một chút kiến thức lập trình cơ bản về JavaScript là có thể viết được GAS. Editor và môi trường chạy tất cả đã có Google lo :D. Google cũng cung cấp bộ tài liệu hướng dẫn khá chi tiết về GAS trên trang web của họ. Bạn có thể truy cập theo đường dẫn này.

IV. Thực thi nhiệm vụ

Sau khi đã có những kiến thức cơ bản về GAS, giờ thì chúng ta hãy quay lại với mục tiêu ban đầu tạo ra 4 bộ lịch về ngày âm của mình.

1. Tìm nguồn dữ liệu:

Theo mình, đây có lẽ là công đoạn mất công nhất. Mình phải vào Google tìm các trang web về ngày đẹp trên mạng. Để được lựa chọn, trang web phải thoả mãn hai tiêu chí sau:

  • Thứ nhất, hỗ trợ tra cứu thông tin của từng ngày qua phương thức GET.
  • Thứ hai, thông tin cung cấp phải đầy đủ, chi tiết và chính xác.

Sau một buổi sáng đỏ mắt tìm kiếm cuối cùng mình cũng đã tìm được một trang web đáp ứng được hai tiêu chí (Cũng vì lý do đây là một blog về kỹ thuật, chúng ta hãy tạm gọi là www.trang-web-ma-ai-cung-biet-la-gi-day.com).

2. Thao tác với Google Spreadsheet

Thực ra, mình không tạo Google Spreadsheet này cũng không sao vì dữ liệu lấy từ trang web www.trang-web-ma-ai-cung-biet-la-gi-day.com có thể lưu trực tiếp vào Google Calendar. Tuy nhiên, để phòng lỡ mai này trang web không còn hoạt động, mình vẫn còn nơi lưu trữ thông tin mà xem lại :).

Bước 1: Để tạo được Google Spreadsheet thì điều đầu tiên là phải có một tài khoản Google. Bạn có thể sử dụng tài khoản hiện tại của bạn hoặc tạo một tài khoản mới tại đây.

Bước 2: Bạn truy cập vào đường dẫn https://docs.google.com/spreadsheets, chọn Blank và nhập tên cho Google Spreadsheet (ví dụ XemNgayDep).

Bước 3: Tạo GAS project to Google Spreadsheet vừa tạo bằng cách truy cập Tools -> Script editor...

Bước 4: Bạn hãy xoá hàm myFunction() mặc định và thêm vào hàm createYearSheet() như sau.

function createYearSheet() {
  var ss = SpreadsheetApp.openById("Google-Sheet-ID");
  var startYear = 2017;
  var endYear = 2027;
  for (var i = startYear; i <= endYear; i++) {
    var curYear = i.toString();
    //Tạo sheet mới với tên là năm
    ss.insertSheet(curYear);
    var curSheet = ss.getSheetByName(curYear);

    // Kiểm tra xem có phải năm nhuận hay không
    var range = (i % 4 == 0) ? curSheet.getRange('A1:E366') : curSheet.getRange('A1:E365');

    //Điền vào cột A, ngày của năm theo format MM/DD/YYYY
    var start = new Date("01/01/" + curYear);
    var end = new Date("12/31/" + curYear);
    var cellIdx = 1;
    while(start <= end){
      range.getCell(cellIdx,1).setValue(start);
      var newDate = start.setDate(start.getDate() + 1);
      start = new Date(newDate);
      cellIdx++;
    }
  }
  //Xoá sheet mặc định Sheet1
  var defaultSheet = ss.getSheetByName("Sheet1");
  ss.deleteSheet(defaultSheet);
}

Hàm createYearSheet() sẽ tạo ra các sheet có tên là các năm trong khoảng thời gian được định nghĩa bởi 2 biến startYearendYear.

Trong dòng code var ss = SpreadsheetApp.openById("Google-Sheet-ID");, Google-Sheet-ID là ID của Google Spreadsheet XemNgayDep vừa tạo. Nhờ có ID này, Google mới phân biệt được Google Spreadsheet của bạn với hàng trăm triêụ tài liệu của những người dùng khác. Bạn có thể lấy ID của Google Spreadsheet trong chính URL của nó. Đó là chuỗi ký tự nằm giữa d//edit.

Ví dụ:

https://docs.google.com/spreadsheets/d/**4U_UZ8y2Swm6YBAZHcm9oVxiCXEs-pEzbDGNXYAjTwtY**/edit#gid=0

Bước 5: Lưu project. Khi đó Google sẽ yêu cầu bạn nhập tên project (ví dụ XemNgayDep).

Bước 6: Chạy hàm createYearSheet() để tạo các sheet từ Menu-bar của project Run -> createYearSheet. Khi đó Google sẽ yêu cầu bạn cấp quyền truy cập vào Google Spreadsheet. Sau khi hàm chạy xong, bạn hãy chuyển sang Google Spreadsheet XemNgayDep để kiểm tra kết quả.

3. Tạo bộ lịch trên Google Calendar

Mục đích chính của bài viết này không phải là 4 bộ lịch "made by Enso" của mình mà là cách thức GAS tương tác với Google Spreadsheet và Google Calendar nên mình sẽ chỉ trình bày về cách tạo bộ lịch Ngày lễ âm lịch.

Bước 1: Truy cập Calendar Settings -> Calendars tab -> Create new calendar.

Bước 2: Calendar Name: NgayLeAmLich (hoặc bất cứ tên nào mà bạn thích).

  • Nếu bạn muốn chia sẻ bộ lịch với tất cả mọi người, bạn có thể chọn Make this calendar public.
  • Nếu bạn chỉ muốn chia sẻ bộ lịch với một số người, bạn có thể thêm email của người đó vào mục Share with specific people.
  • Nếu bạn muốn giữ bộ lịch cho riêng bạn thì không cần làm gì cả. Và một ngày đẹp trời, khi bạn muốn chia sẻ bộ lịch này thì hãy yên tâm rằng những thiết lập này luôn nằm ở đó đợi bạn.

Bước 3: Lấy ID của bộ lịch. Cũng giống như Google-Sheet-ID, Google sử dụng ID này để phân biệt bộ lịch của bạn với những bộ lịch của người khác. Tuy nhiên thì ID của bộ lịch hơi lắt léo khó tìm một chút.

  • Bạn truy cập vào Calendar Settings
  • Trong Calendars tab có danh sách các bộ lịch của bạn. Bạn tìm đến bộ lịch mà bạn vừa tạo bên trên, bấm vào nó.
  • Cuối cùng tìm đến mục Calendar Address và copy lấy chuỗi kí tự bôi đậm như sau:

(Calendar ID: necfejcfetmd9n75on4reaq99c@group.calendar.google.com)

4. Thêm event vào bộ lịch

Trong GAS project, bạn mở file Code.gs và thêm đoạn code dưới đây vào sau hàm createYearSheet()

function createEvents() {
  // NgayLeAmLich calendar
  var ngayleamlichCal = CalendarApp.getOwnedCalendarById('Calendar-ID');
  
  // Mở sheet của từng năm trong Google Spreadsheet.
  var ss = SpreadsheetApp.openById('Google-Sheet-ID');
  var curYear = 2017;
  var sheet = ss.getSheetByName(curYear.toString());
  
  // Kiểm tra xem có phải năm nhuận hay không
  var range = (curYear % 4 == 0) ? sheet.getRange('A1:C366') : sheet.getRange('A1:C365');

  //Chuyển format của 3 cột A, B, C sang plain text
  range.setNumberFormat("@");
  
  var numRows = range.getNumRows();
  for (var i = 1; i <= numRows; i++) {
    // Tạo request URL cho từng ngày trong năm
    var value = range.getCell(i,1).getValue();
    var parts = value.split('/');
    var requestURL = "https://www.trang-web-ma-ai-cung-biet-la-gi-day.com/xem-ngay-tot-xau/" + "ngay-" + parts[1] + "-thang-" + parts[0] + "-nam-" + parts[2];

    // Set link request cho cột B
    range.getCell(i,2).setValue(requestURL);
    
    // Lấy về code HTML kết quả tra cứu ngày đẹp
    var page = UrlFetchApp.fetch(requestURL);
    var pageHTML = page.getContentText();
    var amLich = getLunarDay(pageHTML);
    
    // Set ngày âm lịch cho cột C
    range.getCell(i,3).setValue(amLich);
  }
}

// Hàm tách lấy ngày âm lịch từ code HTML trả về.
function getLunarDay(htmlCode) {
  var pattern = /<td> <b>Ngày<\/b> (.*?) - <span style="color:#990000">/
  var match = pattern.exec(htmlCode)
  return match[1];
}

Dòng code CalendarApp.getOwnedCalendarById('Calendar-ID');, Calendar-ID là ID của bộ lịch NgayLeAmLich vừa tạo.

Dòng code var ss = SpreadsheetApp.openById('Google-Sheet-ID');, Google-Sheet-ID là ID của Google Spreadsheet.

Hàm getLunarDay() sử dụng regular expression tách đoạn xâu kí tự chứa ngày âm lịch trong mã HTML của trang web được trả về. Tiếc là GAS hỗ trợ parse HTML không được tốt nên hiện tại mình thấy regular expression là phương án khả thi nhất.

Sau khi lấy được ngày âm lịch tương ứng cho ngày dương lịch, ta sẽ dựa vào ngày âm lịch này để tạo các sự kiện. Bạn hãy thêm đoạn code bên dưới này vào dưới dòng code range.getCell(i,3).setValue(amLich);

    // Tạo event cac ngay le am lich
    var lunarDay = parseInt(amLich.split('/')[0]);
    var lunarMonth = parseInt(amLich.split('/')[1]);
    
    var leamlichEvent = null;
    if (lunarDay == 1) {
      if (lunarMonth == 1) leamlichEvent = ngayleamlichCal.createAllDayEvent("Tết Nguyên Đán", new Date(value));
      else leamlichEvent = ngayleamlichCal.createAllDayEvent("Mồng một", new Date(value));
    }
    
    if (lunarDay == 15) {
      if (lunarMonth == 8) leamlichEvent = ngayleamlichCal.createAllDayEvent("Trung thu", new Date(value));
      else if (lunarMonth == 7) leamlichEvent = ngayleamlichCal.createAllDayEvent("Vu Lan", new Date(value));
      else if (lunarMonth == 1) leamlichEvent = ngayleamlichCal.createAllDayEvent("Rằm tháng giêng", new Date(value));
      else if (lunarMonth == 4) leamlichEvent = ngayleamlichCal.createAllDayEvent("Rằm - Phật đản", new Date(value));
      else leamlichEvent = ngayleamlichCal.createAllDayEvent("Rằm", new Date(value));
    }
    
    if (lunarDay == 23 && lunarMonth == 12) {
      leamlichEvent = ngayleamlichCal.createAllDayEvent("Ông công ông táo", new Date(value));
    }
    
    if (lunarDay == 5 && lunarMonth == 5) {
      leamlichEvent = ngayleamlichCal.createAllDayEvent("Tết Đoan Ngọ", new Date(value));
    }
    
    if (lunarDay == 10 && lunarMonth == 3) {
      leamlichEvent = ngayleamlichCal.createAllDayEvent("Giỗ tổ Hùng Vương", new Date(value));
    }

    // Reminder bằng email trước 6 tiếng (có nghĩa là 18h00 của ngày hôm trước)
    if (leamlichEvent) leamlichEvent.addEmailReminder(360);

Để tạo event cho bộ lịch, chỉ cần chạy hàm createEvents() qua Menu-bar của project Run -> createEvents và chờ đợi kết quả.

Chỉ có một chú ý nho nhỏ đó là ở đây mình tạo event cho từng năm một dựa vào biến curYear. Thực tế mình không chỉ tạo event cho 1 bộ lịch mà cho hẳn 4 bộ lịch (trong đó bộ lịch Ngày lễ âm lịch ít sự kiến nhất). Trong một lần chạy do Google hạn chế số query tạo event, mình chỉ tạo được sự kiện cho khoảng 9 tháng mà thôi. Nếu muốn bạn có thể tạo thêm một vòng lặp bên ngoài để tạo event cho tất cả các năm :D

Sau khi hàm createEvents() chạy xong, bạn có thể kiểm tra kết quả trong Google Spreadsheet và google calendar.

V. Kết luận

Có lẽ đến đây, bạn đã phần nào thấy được sự tiện dụng của GAS. Nó có thể làm được rất nhiều thứ, tất cả phụ thuộc vào sự phong phú của trí tưởng tượng của bạn. Mình xin được kết thúc bài viết tại đây. Trong phần tiếp theo, mình sẽ trình bày về sự giao lưu và kết hợp giữa GAS và Slack bot. Hẹn gặp lại các bạn trong một tương lai không xa :D.

P/S:

  • Các ID của Google Spreadsheet hay Google Calendar đều là ID không có thật. Mình chỉ sử dụng như ví dụ mà thôi.
  • Cuối cùng mình xin chia sẻ một số hình ảnh về ứng dụng Google Calendar sau khi thêm 4 bộ lịch "made by Enso".



Tài liệu tham khảo