2015年11月19日木曜日

JamTaba2 - ChangeLog 和訳

最近、JamTaba の更新頻度が高いようなので、更新履歴の和訳
Google App Script と Spreadsheet で作ってみた。

JamTaba2 ChangeLog 和訳 / 作業場所 (Google Spreadsheet)



  1. リリースノートから変更履歴を所得 (手作業)
  2. 日付・バージョン番号を整形 (手作業)
  3. リストを翻訳 (手作業)
    1. 組み込み関数の GOOGLETRANSLATE が便利
  4. 更新は不要。自動で反映される
    1. 作業中の内容も反映されてしまうので、断片の見の場合は他の列で
    2. 翻訳文がない場合は、原文を表示

報告予定の既知の問題

  • 日本語の文字化け (JamTaba同士での文字化け。winからの文字は含まない)
  • 入力チャンネルの制限方法 (設定ファイル編集で3つ以上の入力が作れてしまう)
  • ユーザ・スタイルシートの適応が出来ない。
    • Qt 制のプログラムの標準機能 -stylesheet オプションが、
      アプリケーション側で上書きされてしまう

2015年11月15日日曜日

Google Spreadsheet - カスタム関数 EACH_ROW 数行置きに行を取り出す

reshapeのみで出来ないかと頑張ったけどエラーが頻出したので諦めた。
カスタム関数で実行される時は範囲オブジェクトが対象なので、GASで配列を対象とする時では型が違うみたい。

/**
 * @param {Number} step
 * @param {Range} range
 * @param {Range} rows each row step by given number.
 * @customfunction
 */
function EACH_ROW(step, range) {
  return range.filter(function(x, i){ return i % step == 0; });
}

列に対する each col は面倒なので作らない。transposeとの組み合わせで対応。


追記

組み込み関数のみで出来た。範囲からN行毎に行を取り出す
=FILTER(範囲, MOD(ROW(範囲), N) = 0)

2015年11月13日金曜日

Google Spreadsheet - カスタム関数 FLATTEN, CHUNK, RESHAPE

組込であると便利な配列の変形関数群

FLATTEN

2次元配列を1次元配列に変形

/**
 * 2次元配列を配列を平坦にする
 *
 * @param {Range} range 2-dimensional array
 * @return flatted array
 * @customfunction
 */
function FLATTEN(range) {
  return Array.prototype.concat.apply([], range);
}

CHUNK

1次元配列を n要素の2次元配列へ変形
※ この実装では Math.floorで配列長を決めているため、割り切れない場合の末尾は切り捨て

/**
 * 指定された数の要素毎に配列を分割
 *
 * @param {4} n 分割する要素数
 * @param {A1:A12} xs データ・ソースとなる配列
 * @return n要素毎に分割された2次元配列
 * @customfunction
 */
function CHUNK(n, xs) {
  var length = Math.floor(xs.length/n);
  var result = new Array(length);
  for (var i = 0; i < length; ++i) {
    result[i] = xs.slice(i*n, i*n+n);
  }
  return result;
}

RESHAPE

2次元配列の変形

/*
 * 2次元配列の変形
 *
 * @param {Number} row 変形する行数
 * @param {Number} col 変形する列数
 * @param {Range} range 変換対象とする範囲
 * @return 変形後の行列
 * @customfunction
 */
function RESHAPE(row, col, range) {
  var flatted = FLATTEN(range);
  if (Math.floor(flatted.length/col) != row) { throw new Error("サイズが一致しません"); }
  return CHUNK(col, flatted);
}

4列で折り返し表示のような場合は、CHUNK。空行を挟みたい場合などは縦横を入れ替える組み込み関数のTRANSPOSEで空列を含めて変形する、等。

プリミティブな操作なので応用範囲は広いはず。下図は、1x16 の範囲を 4x4 へ変形した例。

2015年11月7日土曜日

Google Spreadsheet - 正規表現を使った数式の一括置換

カスタム関数化とは別のアプローチで、機能修正・追加時の編集コストを抑える方法。
API の setFormula で数式置き換えを検討してたけど、便利な置換機能があった。

正規表現を使って、数式を一括して書き換える。


  1. 対象のセルを選択(範囲若しくは、Ctrl+クリックで複数選択)
  2. [編集][検索と置換]
  3. "数式も検索", "正規表現を使用した検索" にチェック
    1. 大文字込み時にチェックが入りますが、混在している場合は任意で外す
  4. 検索
    1. まず、検索したい文字をコピー&ペースト
    2. そのうち記号の前に \ (円マーク若しくはバックスラッシュ)でエスケープを置く
    3. また、置換時に参照したい文字は丸括弧で囲む
    4. 数式中の空白等、は \s* に置き換え
  5. 置換
    1. 通常通り置換したい文字
    2. 検索の時に括弧で囲んだ中の値を $1 で置き換え
    3. 文字列中にドル記号を含ませたい場合は、その文字をエスケープ

例: 引数の入れ替え

対象の文字列
"FUNC(B1:B2, B3:B4)", "FUNC(C4:C5, C6:C7)", "FUNC(FOO, BAR)"
検索(正規表現)
FUNC\(([\w\:]+)\s*,\s*([\w\:]+)\)
置換
FUNC($2, $1)
置換結果
"FUNC(B3:B4, B1:B2)", "FUNC(C6:C7, C4:C5)", "FUNC(BAR, FOO)"

※ 数式が入れ子になってる場合等は対応できないので、適用できるのは簡単な表現の式のみ。

Google Spreadsheet - カスタム関数 RANDCHANGE

実験中。カスタム関数化は共通の処理を纏めることで、コードの保守性向上を狙うものだけど、
頻繁に行う処理をカスタム関数にする場合のデメリットが、まだ把握できてない。

例えば、多数のセルを参照する場合等は、数式で書いた方がよさそう。
これを多量の引数を取る関数としてしまった場合、
シート側からロジックが把握しにくくなる等の副作用が出てくる。

百分率

整数の場合

整数値でパーセント指定する場合、組み込み関数のRANDBETWEEN(0,99) が使える。

少数の場合

シートで "70%" とパーセンテージ付きの書式で記入すると、
数式上では少数の 0.7 として扱われます。
カスタム関数で確率の判定…するまでもなく、RAND()との比較のみで済みそう。

出番がなさそうだけど。今回は、カスタム関数にする事で注意しないといけない点の考察。

/**
 * 与えられた確率でランダムに bool値 を返す関数
 *
 * @param {float} rate 0.0 以上 1.0 以下の値
 * @return {boolean}
 * @customfunction
 */
function RANDCHANCE(rate) {
  // NOTE: Math.random() は、0 以上 1 未満の値を返します
  return Math.random() <= rate;
}

等号を含める理由は、JavaScript の少数の実装では概数で表現されている為、
桁数が増えた場合、例えば 0.99999999999999999 < 1.0 は false になってしまう。
rateに100%を設定したにもかかわらず、falseになる可能性が出てきます。

まるめ誤差の問題もありますが、実際の用途でそれ程細かな精度を必要としない場合は無視できる程度です。

注意事項

  • 数式上とScript上での数値型の違い
  • 頻繁に呼び出す関数をカスタム関数にする場合、カスタム関数の呼び出しコストに注意。
  • カスタム関数内でSpreadsheetAppのAPI呼び出しすると遅くなるので、数式から引数で渡す。
  • シート上の数式で乱数を、引数としてカスタム関数に渡すことはできない等の制限に注意。

2015年11月4日水曜日

SheetUtil update ver7 - PrefixedPipe

SheetUtil ver7 で、プレフィクス(接頭子)を設定できる PrefixedPipe を追加。

Example:


var pipe = SheetUtil.PrefixedPipe.from("Sheet01", "pre_").to("Sheet02");

pipe.copy("name"); // copy "pre_name to name"


Google Spreadsheet - カスタム関数 RANDCHOICE

数式でランダムに選択する方法のメモ

  • リストから要素をランダムで選ぶ =CHOOSE(RANDBETWEEN(1, 4), "A", "B", "C", "D")
  • 範囲が対象の場合 =INDEX(A:A, RANDBETWEEN(1, 4))
  • カスタム関数で =RANDCHOICE("A", "B", "C", "D")

/**
 * picking a random item from a list
 *
 * @param {"A", "B", "C"} items
 * @return one of choosed element
 * @customfunction
 */
function RANDCHOICE() {
  var xs = Array.prototype.slice.call(arguments);
  return xs[Math.floor(Math.random() * xs.length)];
}

参考URL

Google Spreadsheet - 数式の差分管理

Google App Script のトリガーを使って、Google Drive 上で定期バックアップ


function dump_formulas(gridID, sheetName) {

  // 座標の配列を生成する関数群
  var zip = function(a, b) {
    return a.map(function(x,i){ return [x, b[i]]; });
  };
  var product = function(a, b) {
    return a.map(function(x){ return b.map(function(y){ return y+x; }); });
  };
  var flatten = function(xs) {
    return xs.reduce(function(x,y){ return x.concat(y); }, []);
  };
  var num_range = function(n) {
    return Array(n).join(0).split(0).map(function(_,i){ return i+1; })
  };
  var alpha_range = function(n) {
    var xs = "ZABCDEFGHIJKLMNOPQRSTUVWXY";
    return num_range(n).map(function(x){
      var mod = x % 26,
          pow = x / 26 - (mod ? 0 : 1) | 0;
      return (pow ? xs[pow] : '') + xs[mod];
    });
  };

  // 数式を所得
  var ss = SpreadsheetApp.openById(gridID),
      sheet = ss.getSheetByName(sheetName),
      formulas = sheet.getDataRange().getFormulas();
  
  // 数式に座標を添え、出力形式を整える
  var address = product(num_range(formulas.length), alpha_range(formulas[0].length)),
      source = zip(flatten(address), flatten(formulas)) // [座標, 数式] のペアの配列を生成
        .filter(function(xs){ return xs[1]; }) // 数式のないセルは無視
        .map(function(xs){
          return "// " + xs[0] + "\n\n" + xs[1];
        }).join("\n\n====\n"); // 区切り線で連結

  // 生成日時を行頭へ築城
  var timestamp = Utilities.formatDate(new Date(), 'JST', 'yyyy-MM-dd HH:mm:ss'),
      header = "// " + timestamp + "\n";
  
  return header + source;
}


function backup_formulas() {
  var content = dump_formulas("XXXXXXXX", "Sheet01");

  // ファイル名
  var date = Utilities.formatDate(new Date(), 'JST', 'yyyyMMdd_hhmmss'),
      path = "dump_" + date + ".txt";
  
  // Backupフォルダ内へファイル出力 ※ Backupフォルダが2つ以上ある場合は注意
  var folders = DriveApp.getFoldersByName("Backup");
  if (folders.hasNext()) {
    folders.next().createFile(path, content);
  }
}


  • シートのID/シート名/ファイル・フォルダ名は適宜変更
  • フォルダは予め生成しておきます
  • トリガーに backup_formulas 関数を登録して、定期的な自動バックアップ

実装上の制限

  • alpha_range関数を3桁に対応させてない為、列は A..ZZ までしか扱えません。

2015年11月2日月曜日

Google Spreadsheet - 数式の一括出力


var shell = SheetUtil.Shell.attach("Sheet01");
var join = function(x){ return x.join("\n"); };
var source = shell.sheet.getDataRange().getFormulas().map(join).join("====\n"); 

Logger.log(source);

// テーブルが多い時は、ドライブに保存する
// DriveApp.createFile("dump.txt", source);


数式の一括出力…と、SheetUtil.Shell で提供されてない API を使う時のサンプル。
.sheet プロパティが Sheetオブジェクトを保持しています。

オブジェクトが持つプロパティは Shell.gs の行頭の関数内で確認出来ます。(http://bit.ly/SheetUtil)
[リソース][ライブラリ] から、ライブラリ名をクリックするとドキュメントを表示する機能がありますが、
ドキュメントはまだ書いてません。(TODO)

SheetUtil での Spreadsheet/Sheetオブジェクトのキャッシュ化

ついでに、SheetUtilの仕様の説明

SheetUtil.Shell.attach は、ライブラリ内でキャッシュを持ちます。
2回目以降の同じ名前のシートを呼び出す時、API 呼び出しは発生しません。
SheetUtil.Pipe も同様、キャッシュを共有してます。

キャッシュ化の反作用として、オブジェクトを長期間保持することになる為、
長時間実行されるプログラムでは、その間リソースが開放されないといった面もありますが、
Google App Script として実行する限りでは、実行時間が短いので、顕在化する問題ではありません。

※ キャッシュから解放する dettach メソッドも用意しました。
通常は自動的に開放されるので、明示的な dettach は不要です。

  // var ss = SpreadsheetApp.getActiveShreadsheet();
  // var sheet = ss.getSheetByName("Sheet01");

  var sheet = SheetUtil.getSheet("Sheet01");

ShellやPipeを必要としない、従来のSheetオブジェクトが必要な場合でも、
SheetUtilの提供する関数を使う事で、キャッシュ化の恩恵を得られます。

Google Spreadsheet - カスタム関数 RANGEJOIN

画像アップロードのテストを兼ねて… Google Spreadsheet カスタム関数の作り方メモ

組み込みのJOINは一次元配列のみを対象としていて2行以上の範囲には使えません。
そこで、2行以上の範囲の連結に対応した RANGEJOIN カスタム関数を作ります。

/**
 * RANGEJOIN
 *
 * @param {string} separator 区切り文字
 * @param {Range} range 範囲
 * @return {string}
 * @customfunction
 */
function RANGEJOIN(sep, range) {
  return range.join(sep);
}

カスタム関数作成のガイドライン

Custom Functions in Google Sheets の簡単な訳

命名規則

  • 関数の名前は、組み込み関数と被らないように。
  • アンダースコアで終了する命名は、App Scriptではプライベート関数になる為、
    カスタム関数として呼び出すことができません。
  • function myFunction()
    で定義してください。
    var myFunction= new Function()
    は、カスタム関数としては呼び出せません。
  • スプレッドシートの関数名は、慣例上大文字ですが、大文字小文字の区別はありません。

引数

組み込み関数の様に、カスタム関数も引数を取ることができます

  • 単項のセルを引数に渡した場合、引数はそのセルの値
  • 複数の範囲を引数に渡した場合、引数はその範囲の値の2次元配列
  • 引数には、乱数(RAND())や日付(NOW()) のような、非決定的な値を取ることはできません。`Loading ....` と表示されてしまいます。

戻り値

カスタム関数は、セルに表示する値を返す必要があります

  • 単一の値を返す場合、その値が呼び出し元のセルに表示されます。
  • 2次元の配列であれば、呼び出し元のセルを左上として、隣接するセルが空白ならその範囲に値を展開します。
    もし範囲内のセルに値が既にある場合は、上書きはできず #REF エラーになります。
  • カスタム関数では、呼び出し元のセルとその隣接セルにしか表示出来ない。
    任意のセルに値を渡すには、カスタムメニューを使う。
  • カスタム関数は 30 秒以内に値を返さなくてはなりません。30秒以上経過すると、
    `Internal error executing the custom function.`とエラーが表示されます。

データ型

Google Spreadsheet では、データを異なる形で保有している為、 カスタム関数として呼び出された場合とApp Scriptとして呼び出された場合で異なるデータになる場合があります。

  • シート上での時刻は、スクリプト上ではDateオブジェクトとして扱われます
  • パーセントの値は、シート上では整数で現されますが、
    スクリプト上では decimal 型の少数になります。
    例えば、10% のセルの値は、スクリプトでは 0.1

Autocomplete

入力補完。記述しておくと、セル編集時に、関数名の候補や引数の説明を表示してくれるようになります。

  • JsDoc で @customfunction タグをつける。

参考サイト


追記

より汎用的にするなら、JOIN関数と組み合わせて使うことができるように、
複数行の範囲を、一次元の配列にするカスタム関数を作る。

function FLATTEN(range){
  return range;
}

例 =JOIN(",", FLATTEN(A1:B2))

SheetUtil update ver5

SheetUtil.Shell

  • getValues/setValuesに対応した、メソッド追加 gets/sets

SheetUtil.Shell

  • コピー先の範囲が同じ場合は、省略可能に変更。

var pipe = SheetUtil.Pipe.from("Sheet01").to("Sheet02");
pipe.copy("something_long_named_range_name");


ライブラリのアップデート方法は、[リソース][ライブラリ] から、バージョン番号を選択するだけ。
問題があれば以前のバージョンに戻せるように、現在のバージョン番号は控えておく。

TODO

  • Google App Script のユニットテストの方法を調べる

2015年11月1日日曜日

Google App Script / MusicXML メタ情報の追加

ライブラリ更新

第二引数に関数を渡してルート要素を操作できるように変更。ここでヘッダ情報を追加する。
特定の情報のみに対応した場合、他の情報を追加したい時、ライブラリ側の対応が必要になるので、
コールバック関数を渡す形式にして、自由に情報を追加できるようにした。


var addHeaderCallback = function(root) {

  // ヘッダ情報のコンテナとなる identification要素を作成
  var identification = LibMusicXML.createIdentificationElement();
  
  // エンコーディングに関する情報
  var encoding = LibMusicXML.createEncodingElement({
    encoder: "",
    software: "",
    description: "",
  });

  // 独自情報を追記
  var misc = LibMusicXML.createMiscElement({
    "rm-version": "1.02",
    "rm-firstkey": "C",
    "rm-lastkey": "Cm",
  });

  // 各要素を親へ追加する
  identification.addContent(encoding);
  identification.addContent(misc);
  root.addContent(0, identification);

  // 題名と作業番号
  root.addContent(0, LibMusicXML.createWorkElement("TITLE", "1");

};

var xml = LibMusicXML.toMusicXML(LibMusicXML._flatten(rows), addHeaderCallback);

identification 要素へ追記するメタ情報としては、他に、"source", "rights", "relation" 等が有り、
Dublin Core を参考にしている。 参考サイト: Dublin Core: メタデータを記述するボキャブラリ

関数名が長いけど補完が効くので、識別子の部分を短くすれば、キータイプ量は減らせます。