tetsunosukeのnotebook

tetsunosukeのメモです

Google Apps ユーザーフォーラムでSidebarガジェットの発表をしてきました(前編)

Google Apps ユーザーフォーラムへ行ってきたよ

会社の人事総務部で導入しているGoogleAppsのユーザフォーラムが開催(主催:株式会社電算システム様)されたので、ちょっとお手伝いに行ってきてついでに(?)発表してきました。

弊社では、社員の内線番号を氏名から検索できるGmail Sidebar ガジェットを導入しています。

このお話を5分程度させていただいたところ、思いの外好評でしたので、そのノウハウを書いておきます。

今回作ってみるガジェットについて

GoogleSpreadSheetにデータを保存し、そのデータをSQLのような方法で取得し、検索をするという動作をするものです。

Gmail Sidebar ガジェットについて

Sidebar ガジェットは、GmailのLabsを有効にし、”Add any gadget by URL” という機能を有効にして利用します。ここにガジェット自体のXMLを登録することで、サイドバーに表示されるというものです。ガジェットは一般的なガジェット(iGoogleなどに追加できるガジェット)のすべてを登録できます。

今回はその自作ガジェットの超基本についてです。

極めてシンプルなガジェットを作ってみました。

<?xml version="1.0" encoding="utf-8"?>
<Module>
<ModulePrefs title="はじめてのSideBarGadget"
             description="サイドバーにhello gadgetと時刻を表示します"
             author="ITO Tetsunosuke"
             author_email="tetsunosuke.ito@photocreate.co.jp"
/>
<Content type="html">
<![CDATA[
  <p>hello gadget!</p>
  <p id="time"></p>
  <script type="text/javascript">
  // 時刻を生成して、id="time"の中に表示
  var d = new Date();
  var textNode = document.createTextNode(d.toString());
  document.getElementById("time").appendChild(textNode);
  </script>

]]>
</Content>
</Module>

CDATAの中にHTMLやJavaScriptを書くことができます。
このガジェットファイルをXMLとして保存し、Webサーバにアップします。今回はHTTPS領域にファイルを置きたいため、DropBoxを利用しました。ファイルの実体はこちらにあります:https://dl.dropbox.com/u/2374674/googleappsuserforum/googlesidebargadget.xml

SpreadSheetにアクセスする

「ガジェットから」を一旦忘れ、単なるHTML+JavaScriptでGoogleSpreadSheetとのデータのやりとりをしてみます。

SpreadSheetとのやりとりは、Visualization APIを用います。

まずは仮のデータとして日本の歴代総理大臣のシートを作った(けど10代で挫折した)ので、こちらのデータを取得して表示してみるプログラムを作ってみます。

歴代総理大臣のシート: https://docs.google.com/spreadsheet/ccc?key=0AvdfXiBhjpT3dHdNY1ZIV053Nnd6d0s5Y240Z0h1Znc#gid=0

プログラムはこちらで実行、ソースの確認ができます:https://dl.dropbox.com/u/2374674/googleappsuserforum/spreadsheet.html

<html>
<head>
  <script type="text/javascript" src="https://www.google.com/jsapi"></script>
  <script type="text/javascript">
      google.load("visualization", "1");
      google.load("jquery", "1.4.2");
      // 読み込み完了したらinitメソッドを呼ぶ
      google.setOnLoadCallback(init);
      
      
      // クエリ実行後のコールバック
      function handleQueryResponse(response) {
        var data = response.getDataTable();
      
        // 結果を一行ずつ処理
        for (var row = 0; row < data.getNumberOfRows(); row++) {
            a = data.getFormattedValue(row, 0); // 1列目つまりA列
            b = data.getFormattedValue(row, 1); // 2列目つまりB列
            c = data.getFormattedValue(row, 2); // 3列目つまりC列
        
            line = ("苗字=" + a + ","
                  + "名前=" + b + ","
                  + "番号=" + c);
            p = "<p>" + line + "</p>";
            // HTMLのid="result"にこの結果を追加していく
            $("#result").append(p);
        }
      }
      
      // 読み込み完了時に実行される
      function init() {
        // スプレッドシートのURL
        var spreadsheet_url = "https://docs.google.com/spreadsheet/ccc?key=0AvdfXiBhjpT3dHdNY1ZIV053Nnd6d0s5Y240Z0h1Znc";
        var query = new google.visualization.Query(spreadsheet_url);
        // 引っ張ってくる列
        query.setQuery("select A, B, C");
        // クエリを実行し、handleQueryResponseメソッドを実行
        query.send(handleQueryResponse);
      }
  </script>
</head>
<body>
<div id="result"></div>
</body>
</html>

今回利用するのはVisualizationAPIですが、その他にHTMLに対して簡単にアクセスしたいため、jQUeryも利用することにします。Google AJAX API を使えば、これらのライブラリを簡単に利用することができます。

それがこの部分です。

  <script type="text/javascript" src="https://www.google.com/jsapi"></script>
  <script type="text/javascript">
      google.load("visualization", "1");
      google.load("jquery", "1.4.2");
      // 読み込み完了したらinitメソッドを呼ぶ
      google.setOnLoadCallback(init);

google.setOnLoadCallbackで、読み込み完了後に実行される処理、つまり今回行いたい処理を記載します。

細かいことは一旦気にせず、スプレッドシートのURLを用いて、その表をまさにデータベースのテーブルの用に扱い、SQLのようなクエリでデータを検索、クエリを実行して結果が取得できたら、その結果を用いて一行ずつデータを取りだしていく、という処理を行なっています。

VisualizationAPIを使うと、非常に簡易的なデータベースを構築できるので、こういった用途にはオススメです。

これらを組み合わせて、内線表ガジェットができていくのですが、続きは後編で。

数値の割り算でハマる

PHPPostgreSQLに投げてfloorを計算させたら結果が違うんですっ!」

とか言われてちょっと何言ってるかわからない状態になった。

どうもこういうことらしい。int(に見えるリテラル)をintで割ったときの挙動の問題のようだ。

PHPの floor(-155/10)の結果

$ php -r "echo floor(-155/10);"
-16

PostgreSQLのfloor(-155/10) の結果

# select floor(-155/10);
 floor 
-------
   -15
(1 row)

おっとっと。


というわけで、floorをかける前の値が何になっているか調べてみる。

PostgreSQLでは

# select -155/10, 1555/10.0, floor(-155/10);
 ?column? |       ?column?       | floor 
----------+----------------------+-------
      -15 | 155.5000000000000000 |   -15
(1 row)

PHPでは

$ php -r "var_dump(-155/10);"
float(-15.5)

なるほど。PostgreSQLはint同士の計算をintに変換するがPHPではしないのだな。

ちょっと気になったので他の言語でもやってみる

Ruby:

irb(main):001:0> (-155/10).floor
=> -16
irb(main):002:0> -155/10
=> -16

Python:

>>> import math
>>> math.floor(-155/10)
-16.0
>>> -155/10
-16
# -15になる処理系も確認されている。詳細不明

JavaScript:

> -155/10
-15.5
> Math.floor(-155/10)
-16

JavaScriptにはそもそもNumberという概念しかないから型変換もクソもないようだ。

(メモ) jsTestDriver で HtmlDocを使う

jsTestDriverではテストコードの中にHTMLのDOM要素へアクセスできるらしいので、やってみた。下記のように書く。

TestCase("domTest", {
  setUp: function() {
  },
  tearDown: function() {
  },
  
  "test inner div document": function() {
      /*:DOC div = <div><p id="foo">bar</p></div>*/
      tagP = this.div.getElementsByTagName("p");
      assertEquals("bar", tagP[0].innerText);
  }
});

ポイントは、DOCで宣言したものには "this.div"のようにして参照するということ。

つまり、たとえば

function getDivValue(id)
{
  return document.getElementById(id).innerText;
}

みたいな関数を書いてしまうと、documentっていうグローバル変数にアクセスするため、テストしづらいということになりそうな感じ。

function getDivValue(dom, id) 
{
  return dom.getElementById(id).innerText;
}

みたいにしろってこと??

そうすれば

TestCase("domTest", {
  "test inner div document": function() {
      /*:DOC div = <div><p id="foo">bar</p></div>*/
      assertEquals("bar", getDivValue(this.div, "foo"));
  }
});

こんな感じに書けるということかな・・・?

ファイルからメソッドを抜き出して分割しまくってやるぜ

複数のファイルに全く同じメソッドを発見してしまいキレたの巻。

メソッドごとに切り出してそれが同じものであればコピペしやがったなこのやろうしけいだ!ってできるかな、と思った。まあ昨日の流れでphpcpdとかでチェックする話なのだろうけど、いざチェックしたとして、それを関数レベルでリファクタリングしていくのに有用ではないか、と感じたわけです。

とりあえず分解対象のクラスを作る。

<?php
class Sample
{
    public function __construct()
    {
        echo "__construct";
    }

    private function privateFunction($param1)
    {
        echo "privateFunction";
    }

    public function publicFunction($param1)
    {
        echo "publicFunction";
    }

    /**
     * staticなFunctionです
     */
    public static function staticFunction($param1, $param2 = 10)
    {
        echo "staticFunction";
    }
}

下記のコードでそれを分解します。
ファイルを読み込んでZend_Reflection_Fileを使うと、そのファイルの中にいくつクラスがあるか?などを調べてくれます。

その中で、クラスから関数を探索し、その関数の属性にあたる情報を下記のようにして取得できます。

<?php
$path = "Sample.php"
require $path;

$autoloader = Zend_Loader_Autoloader::getInstance();   
$ref = new Zend_Reflection_File($path);
// 後で行レベルで内容を調べるため保持
$lines = file($path);

foreach($ref->getClasses() as $cls) {
    // Zend_Reflection_Method
    $methods = $cls->getMethods();
    foreach($methods as $method) {
        // 修飾子
        $modifiers = join(" ", Reflection::getModifierNames($method->getModifiers()));
        // 関数名
        $name = $method->getName();

        // パラメータ
        $parameterNamesArray = array();
        $params = $method->getParameters();
        foreach($params as $param) {
            $parameterNamesArray[] = "$" . $param->getName();
        }
        $parameters = join(", ", $parameterNamesArray);

        // シグネチャ
        $signature = sprintf("%s function %s (%s)", $modifiers, $name, $parameters);

        // 本体は行レベルで取得する
        $body = join("", array_slice($lines, $method->getStartLine(), $method->getEndLine() - $method->getStartLine()));

        // 出力
        echo $signature . "\n" . $body . PHP_EOL;
    }
}

タブとかはもちろん吹き飛んでしまうので出力はこんな具合になる。

public static function staticFunction ($param1, $param2)
    {
        echo "staticFunction";
    }

これを何らかの命名規則のファイル(filepath_class_method.txtみたいな)に書きこんで、md5sumとかdiffとかで調べていけば、共通している部分の抽出ができるのではないか、と思う。

ちなみにこれをかけてみたら200行にも渡るファイルが発見され、それはそれでどうなんだ、ってなってるとこ。

phpcpd だとCopy Paste Detectをあまりしてくれない件、でjenkinsで困った。

phpcpdとpmd

とりあえずコピペした下記のような2つのクソコードを作ってみる。
Dollar.php

<?php

class Dollar
{
    private $prefix;
    private $value;
    public function __construct($value)
    {
        $this->prefix = "$";    
        $this->value = $value;
    }

    public function __toString()
    {
        return $this->prefix . $this->value;
    }
}

Yen.php

<?php

class Yen 
{
    private $prefix;
    private $value;
    public function __construct($value)
    {
        $this->prefix = "\\";   
        $this->value = $value;
    }

    public function __toString()
    {
        return $this->prefix . $this->value;
    }
}

これをチェックしよう、と思うがとても残念なことになった。むしろ重複しまくっていると検出してくれないのか??

$ phpcpd .
phpcpd 1.3.5 by Sebastian Bergmann.

0.00% duplicated lines out of 36 total lines of code.

Time: 0 seconds, Memory: 1.25Mb

というわけで、本家pmdを使うことにする。

$ run.sh cpd --files . --minimum-tokens 75 --language php
Found a 7 line (78 tokens) duplication in the following files: 
Starting at line 9 of /var/lib/jenkins/jobs/test/workspace/svn/src/Test/./Dollar.php
Starting at line 9 of /var/lib/jenkins/jobs/test/workspace/svn/src/Test/./Yen.php

        $this->prefix = "$";    
        $this->value = $value;
    }

    public function __toString()
    {
        return $this->prefix . $this->value;
=====================================================================
Found a 5 line (75 tokens) duplication in the following files: 
Starting at line 5 of /var/lib/jenkins/jobs/test/workspace/svn/src/Test/./Dollar.php
Starting at line 5 of /var/lib/jenkins/jobs/test/workspace/svn/src/Test/./Yen.php

    private $prefix;
    private $value;
    public function __construct($value)
    {
        $this->prefix = "$";    

jenkinsのタスクに入れよう

で、これをjenkins経由で入れようとして、build.xmlを変更。

 <target name="cpd" description="Find duplicate code using PMD-CPD">
  <exec executable="pmd/bin/run.sh">
   <arg value="cpd" />
   <arg value="--language php" />
   <arg value="--minimum-tokens 75" />
   <arg value="--files ${basedir}/svn/src" />
   <arg value="--format xml" />
   <arg value=" &gt; ${basedir}/build/logs/pmd-cpd.xml" />
  </exec>
 </target>

ところが、これだと、 "> ファイル" の部分がうまく動いてくれない。
(pmdにそもそもファイルに出力するオプションがあればいいのに標準出力をパイプしないといけない)

おそらくその原因はpmd以下のrun.sh が原因。

java "${HEAPSIZE}" -cp "${classpath}" "${CLASSNAME}" ${@}

こんな具合にしたら通った

command="java ${HEAPSIZE} -cp ${classpath} ${CLASSNAME} ${@}"
$command
$ ant cpd
Buildfile: build.xml

cpd:

BUILD SUCCESSFUL
Total time: 0 seconds

分散分析を行う

下記のような表があったとき(はてな記法でcolspanのやつとかできないのね!めんどくさい!)
要因1,要因2の影響について調べる。


















要因1
要因2ABCD
a34415256
b35465562
c40535663

このデータ、その形のままだとRに取り込みにくいのでこんな形の単純な表(データフレーム)にする。

   factor1 factor2 data
1        a       A   34
2        a       B   41
3        a       C   52
4        a       D   56
5        b       A   35
6        b       B   46
7        b       C   55
8        b       D   62
9        c       A   40
10       c       B   53
11       c       C   56
12       c       D   63

そのためには下記のように。関数repは同じ回数分繰り返したデータを作ってくれるので、aを連続したければ rep("a", n)のようにする。

> factor1 = c(rep("a", 4), rep("b", 4), rep("c", 4))
> factor2 = rep(c("A", "B", "C", "D"),3)
> data = c(34, 41,52,56,35,46,55,62,40,53,56,63)
> data
> f = data.frame(f1=factor1, f2=factor2, data)

これに対してaovで分散分析を行う

> summary(aov(data~f1+f2, data=f))
            Df Sum Sq Mean Sq F value   Pr(>F)    
f1           2  105.2    52.6   12.37  0.00743 ** 
f2           3  966.3   322.1   75.78 3.68e-05 ***
Residuals    6   25.5     4.2                     
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1

すると、P値が5%より低く、f1もf2もこの要因として適している、と分析できる。多分この***の多いf2の要因のほうが強いのだろう。

試しに、明らかに要因1の影響しか受けないこんなデータでやると、

> data = c(1, 1,1,1,2,2,2,2,3,3,3,3)
> f = data.frame(f1=factor1, f2=factor2, data)
> f
   f1 f2 data
1   a  A    1
2   a  B    1
3   a  C    1
4   a  D    1
5   b  A    2
6   b  B    2
7   b  C    2
8   b  D    2
9   c  A    3
10  c  B    3
11  c  C    3
12  c  D    3
> summary(aov(data~f1+f2, data=f))
            Df Sum Sq Mean Sq   F value Pr(>F)    
f1           2      8       4 3.865e+31 <2e-16 ***
f2           3      0       0 1.000e+00  0.455    
Residuals    6      0       0                     
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘

f1のP値のみが小さく、f2は45%とかなり大きいのでf2の影響はないと言える

このデータを回帰分析してみよう

要するにa,b, A,B,C のみでこのデータは決まるわけなので、数量化理論一類にて、下記のような表と見なせる

a b A B C data
1 0 1 0 0 34
1 0 0 1 0 41
1 0 0 0 1 52
1 0 0 0 0 56
0 1 1 0 0 35
0 1 0 1 0 46
0 1 0 0 1 55
0 1 0 0 0 62
0 0 1 0 0 40
0 0 0 1 0 53
0 0 0 0 1 56
0 0 0 0 0 63

これをデータフレームにして

> a = c(1,1,1,1,0,0,0,0,0,0,0,0)
> b = c(0,0,0,0,1,1,1,1,0,0,0,0)
> A = c(1,0,0,0,1,0,0,0,1,0,0,0)
> B = rep(c(0,1,0,0), 3) # 早速使ってみた
> C = rep(c(0,0,1,0), 3) 
> data.frame(a=a,b=b,A=A,B=B,C=C, data=data)
   a b A B C data
1  1 0 1 0 0   34
2  1 0 0 1 0   41
3  1 0 0 0 1   52
4  1 0 0 0 0   56
5  0 1 1 0 0   35
6  0 1 0 1 0   46
7  0 1 0 0 1   55
8  0 1 0 0 0   62
9  0 0 1 0 0   40
10 0 0 0 1 0   53
11 0 0 0 0 1   56
12 0 0 0 0 0   63


線形回帰分析する

> summary(lm(data~a+b+A+B+C))

Call:
lm(formula = data ~ a + b + A + B + C)

Residuals:
    Min      1Q  Median      3Q     Max 
-2.0000 -1.0417 -0.2917  1.3333  2.7500 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)   63.917      1.458  43.846 9.42e-09 ***
a             -7.250      1.458  -4.973 0.002518 ** 
b             -3.500      1.458  -2.401 0.053224 .  
A            -24.000      1.683 -14.258 7.44e-06 ***
B            -13.667      1.683  -8.119 0.000187 ***
C             -6.000      1.683  -3.565 0.011862 *  
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 

Residual standard error: 2.062 on 6 degrees of freedom
Multiple R-squared: 0.9768,     Adjusted R-squared: 0.9574 
F-statistic: 50.42 on 5 and 6 DF,  p-value: 8.03e-05 

b,Cを抜いて

> summary(lm(data~a+A+B))

Call:
lm(formula = data ~ a + A + B)

Residuals:
    Min      1Q  Median      3Q     Max 
-4.1667 -2.6667 -0.1667  2.4583  4.5000 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)   59.167      1.646  35.952 3.92e-10 ***
a             -5.500      2.208  -2.491  0.03746 *  
A            -21.000      2.550  -8.237 3.54e-05 ***
B            -10.667      2.550  -4.184  0.00306 ** 
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 

Residual standard error: 3.606 on 8 degrees of freedom
Multiple R-squared: 0.9052,     Adjusted R-squared: 0.8696 
F-statistic: 25.46 on 3 and 8 DF,  p-value: 0.0001912 

data = -5.5 * a - 21 * A - 10 * B + 59.167 の回帰式を得ることができた。

たとえば aとCの要因があるデータは、 data = -5.5 + 59.167 = 53.667 と求めることができるし、(実データは52)、cとDの場合は59.167(実データは63)となる。

等分散でないときのt検定(ウェルチのt検定)

同様に、下記のようなデータに対して分析をしてみます。

> a
 [1] 8.81 8.35 8.62 9.11 8.38 9.15 9.22 8.20 9.38 7.57
> b
 [1] 8.27 8.05 8.32 8.08 8.95 8.22 7.81 8.43 8.21 8.17

まずはF検定

> var.test(a, b)

        F test to compare two variances

data:  a and b 
F = 3.5907, num df = 9, denom df = 9, p-value = 0.07053
alternative hypothesis: true ratio of variances is not equal to 1 
95 percent confidence interval:
  0.8918783 14.4561248 
sample estimates:
ratio of variances 
          3.590697 

検定の結果、両側検定のP値が10%を下回っているので、AとBは等分散ではないとみなせます。

この場合、t検定はvar.equal=T を付けないt.testを用います。実行すると「Welch」というのが出てきますね。

> t.test(a, b)

        Welch Two Sample t-test

data:  a and b 
t = 2.12, df = 13.652, p-value = 0.05284
alternative hypothesis: true difference in means is not equal to 0 
95 percent confidence interval:
 -0.006049496  0.862049496 
sample estimates:
mean of x mean of y 
    8.679     8.251 

ここでの両側検定のP値は0.05284なので、片側に直すと、0.02642。したがって5%以下なので、Bの平均がAの平均と等しい仮説は棄却され、Bの平均がAの平均よりも小さいといえます。