2011/02/05

Google ドキュメントを活用する (家計簿編3)


前回でほぼ完成した家計簿ですが、月ごとに自動的にデータを分けておきたいですよね。今回は月ごとにデータをまとめる関数を実装したいと思います。家計簿編はこれで完結です。簡単に実装できると思ったんですが、意外にはまりました。

実装したい内容は、フォームから入力されたデータを保存するシートをコピーして名前を変える、コピー元のシートの古いデータを消去、先月の残高を繰越金として入力する、だけなのですが、いざ繰越金を入力しようとすると、セルに値を書くことは出来るのですが、フォームから入力されたデータとして認識してくれません。つまり、フォームからデータを入力すると折角書いた繰越金が上書きされて消えてしまいます。フォームを管理するスプレッドシートを開くと、メニューバーのフォームの隣りに入力されたデータ数がカッコに囲まれて表示されています。フォームからデータを入力するとインクリメントされますが、普通にスプレッドシートにデータを setValue() で書き込んでもここは変化しません。
データ数をコントロールする API が分かればよいのでしょうが、そんなのは見つかるはずも無いだろうということで、フォームにスクリプトから入力する方法を模索してそれを実装しました。

使った API は UrlFetchApp.fetch(url, optAdvancedArgs) です。

引数の url と optAdvancedArgs はフォームのソースコードから見つけます。HTML でフォームを作ったことがある方なら分かると思いますが、
< form action="~~~" >
で示される URL です。ここで注意すべき点は、url に含まれる formResponse?formkey= の部分で、formResponse?hl=ja&formkey= なっている場合があります。hl=ja&は消しましょう。理由はよくわかりませんが、これを消さないとうまく動作しません。
また optAdvancedArgs を使った繰越金(収入欄)の指定は、< input type="text" name="entry.1.single" でも分かるように、entry.x.single で示されるネームへの引数指定になります。その他の変数も併せて、適宜個々の環境に合わせて設定して下さい。

では前回のスクリプトの代わりに以下の完成版のスクリプトをコピペして置き換えて下さい。解説は特に書きませんので、コメントを参考に理解して下さい。

//-------------------------------------------------------------------
// 家計簿スクリプト for Google docs
// Date: 2/4/'11
// Author: Tatsuro
//-------------------------------------------------------------------

// 定数定義
var UPDATE_DATE = 1;  // バックアップを実行したい日
var BALANCE_COL = 2;  // 残高の列番号B
var IN_COL      = 3;  // 収入の列番号C
var EX_COL      = 4;  // 支出の列番号D
var IN_COL_SUM  = 7;  // 収入合計の列番号G
var EX_COL_SUM  = 8;  // 支出合計の列番号H

// FORM の情報
var FORM_URL    = 'http://spreadsheets.google.com/formResponse?formkey=dGg2dnoyN3pKaEJtb2FfMFF5R3JmNEE6MQ&ifq';
var FORM_TAG_IN = 'entry.1.single=';  // 収入
var FORM_TAG_C1 = '&entry.3.single='; // 適用
var FORM_TAG_C2 = '&entry.4.single='; // 費目
    
//-------------------------------------------------------------------
// accessForm(value_form):
// 新規シートのセットアップのために繰越金(value_form)をフォームを経由して書き込む
//-------------------------------------------------------------------
function accessForm(value_form) {
  // Form に入力したい項目を設定する
  var post_str = 
      FORM_TAG_IN+encodeURIComponent(value_form)+
      FORM_TAG_C1+encodeURIComponent("先月")+
      FORM_TAG_C2+encodeURIComponent("繰越");
  var options = {
    "method"  : "post",
    "payload" : post_str
  };

  // Form にデータを POST する  
  UrlFetchApp.fetch(FORM_URL, options);
}

//-------------------------------------------------------------------
// backupLatest():
// 毎月一度の集計時に呼ばれることを前提とした関数
// 先月分をシートごとコピー、シート名変更、フォームのデータが保存されるシートの初期化を行う
//-------------------------------------------------------------------
function backupLatest() {
  // 今日の日付を取得する
  var today = new Date();

  // 日付がバックアップを実行したい日のみ実行
  if (today.getDate() == UPDATE_DATE) {
    // 左端のシートをアクティブにし、アクティブなシートの複製をつくる
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    ss.setActiveSheet(ss.getSheets()[0]);
    ss.duplicateActiveSheet();
    
    // 複製されたシート(左から2番目)をアクティブにする
    ss.setActiveSheet(ss.getSheets()[1]);
    
    // 複製されたシートのセル A2 のデータを取り出す(日付)
    var date_str = ss.getSheets()[1].getRange(2,1).getValue();
    // 複製されたシートのセル B1 のデータを取り出す(残高)
    var balance = ss.getSheets()[1].getRange(1,BALANCE_COL).getValue();
    
    // 複製されたシートの日付のデータから Date オブジェクトを生成し、年、月を抽出する
    // 文字列解析がめんどうなので(いや、正しい対処法か ww)
    var date = new Date(date_str);
    var year = date.getFullYear();
    var month = date.getMonth() + 1;
    
    // 複製されたシート名を年-月に変更する
    // (既に存在するシート名ではエラーになるので注意)
    ss.renameActiveSheet(year+'-'+month);

    // フォーム入力用のシート(左端のシート)を初期化する
    // 左端のシートのオブジェクト取得
    var s_new = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
    // 左端のシートでデータが書かれている最大の行数を取得する
    var maxRow = s_new.getLastRow();
    // 先月のデータを消す
    s_new.deleteRows(2, (maxRow-1));

    // 繰り越し入力
    accessForm(balance);
  }
}

//-------------------------------------------------------------------
// updateCalculation():
// フォーム入力後に呼ばれることを前提とした関数
// 残高の計算式を埋め込みアップデートする
//-------------------------------------------------------------------
function updateCalculation() {
  // 左端のシート(今月の家計簿・フォームのデータ保存先)のオブジェクトを取得する
  var s_latest = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];

  // 左端のシートでデータが書かれている最大の行数を取得する
  var maxRow = s_latest.getLastRow();

  // 支出の合計を計算したいので、その範囲を A1 形式の文字列にして準備する
  // 範囲は、支出列の入力したばかりの行と前回の支出合計欄
  var sumRngOutC = s_latest.getRange(maxRow,  EX_COL    ).getA1Notation();
  var sumRngOutL = s_latest.getRange(maxRow-1,EX_COL_SUM).getA1Notation();

  // 支出の縦合計の計算式を保管したいセルに先程の A1 形式を使っ埋め込む
  // 保管先のセルは入力したばかりの行の支出合計欄
  // 最初の行の場合は前回の結果が無いので今回の値を参照するだけ
  if (maxRow == 2) {
    s_latest.getRange(maxRow, EX_COL_SUM).setFormula('='+sumRngOutC);
  } else {
    s_latest.getRange(maxRow, EX_COL_SUM).setFormula('=SUM('+sumRngOutC+'+'+sumRngOutL+')');
  }
  
  // 収入の欄も同様に処理する
  var sumRngInC = s_latest.getRange(maxRow,  IN_COL    ).getA1Notation();
  var sumRngInL = s_latest.getRange(maxRow-1,IN_COL_SUM).getA1Notation();

  if (maxRow == 2) {
    s_latest.getRange(maxRow, IN_COL_SUM).setFormula('='+sumRngInC);
  } else {
    s_latest.getRange(maxRow, IN_COL_SUM).setFormula('=SUM('+sumRngInC+'+'+sumRngInL+')');
  }

  // 残高の計算のため、その元となる最後に入力した行の収入合計、支出合計、
  // 最後の行の残高の保管セルの位置を A1 形式の文字列で準備する
  var sumOut    = s_latest.getRange(maxRow, EX_COL_SUM).getA1Notation();
  var sumIn     = s_latest.getRange(maxRow, IN_COL_SUM ).getA1Notation();
  var refResult = s_latest.getRange(maxRow, BALANCE_COL ).getA1Notation();

  // 残高の計算式を埋め込む
  s_latest.getRange(maxRow, BALANCE_COL).setFormula('='+sumIn+'-'+sumOut);

  // 残高をフォームに表示する為、残高の計算結果の参照の式をセルにオーバーライド
  s_latest.getRange(1, BALANCE_COL).setFormula('='+refResult);
}
​

backupLatest() を単独で実行すると期待通りの動作を行います。上手く実行出来ない場合は、定数 UPDATE_DATE が実行したい日の指定に合っているかとどうか確認して下さい。UPDATE_DATE はスクリプトを実行したい日です。例えば日付が1日に変わった直後にバックアップをとれば丁度一月区切りで都合が良いと思います。

ところで、月に一度これを実行するにはどうすればよいかというと、前回使用したトリガーで、そのきっかけをフォームでなく、時間(Time-driven)で指定することで実現可能です。Time-driven は Minutes, Hour, Day そして Week をきっかけに起動できますが、Day timer の時間指定 (例えば夜中の0:00-1:00 の間)で自動でスクリプトを起動し、UPDATE_DATE と日付が合致した場合に限り残りの作業を行うようにします。
Time-driven トリガーを設定する前に必ずスクリプトのプロパティでタイムゾーンを合わせましょう。デフォルトでは日本になっていないようなので、注意です。また、タイムゾーンを設定する前に Time-driven トリガーを設定すると異なるタイムゾーンの設定が残ってしまいますので、その場合は一旦設定したトリガーを削除して、タイムゾーンを設定後にトリガーも登録しなおして下さい。

プロパティのメニュー




タイムゾーン設定

トリガー設定
以上でザックリですが、オートバックアップ付きの家計簿の例の紹介を終わります。まだ実験レベルでの提供による時間で起動できるトリガーも使えるというのは衝撃的事実でした。Google Apps Script にて Cron もどきの機能が使えるということは夢が広がりますね。

2 件のコメント:

匿名 さんのコメント...

野球チームの選手に素振り練習報告書フォームを作りたいのですが
だれが
いつ
何回しているか
今週のトップは誰か?
など、リアルにチームメイトと共有したいのですが
どのように作ればよいでしょうか?
教えてください、よろしくお願いいたします。

たつるー さんのコメント...

チームメイト同士のモチベーションを上げる方法として、面白そうな試みですね。

では、あまり複雑なことは好まない前提とします。
誰が、何回しているという情報を入力するフォームを用意します。このフォームでスプレッドシートには入力した日付、誰、何回という情報が逐次追加されることになります。

まずは、このスプレッドシートそのものを公開することで、まずはリアルタイムに(実際は入力から反映まで若干のタイムラグがあるような気がしますが)誰が、いつ、何回しているかを示すことはできます。

ここまでで必要なデータは既にそろっているので後は集計をいかに自動化するかというところにかかると思います。

スプレッドシートは Excel と同じような表計算機能を既に持っていますので、既存の関数を使って、データが蓄積されているシート(デフォルトでシート1)とは別なシートに集計結果を表示させればよいと思います。既存の関数を使えばその集計用のシートを開いた時点で自動的に集計結果を表示してくれることになります。

とりあえず上記がアイディアの概要です。

従って既存の関数で、今週のトップが誰かという計算式を作れるかが次のキーになります。無ければ Google App Script で作ればよくて、自前の関数をスプレッドシートに埋め込むことにより、期待のシートができると思います。
どんな関数を組み合わせればうまく行くかとか、独自の関数を用意すればいいかはちょっと考えてみないとわかりませんが、実現は可能だと思います。