txqz blog

3944件中1から15件目を表示します。このリソース群のタイトルリスト、またリソースのAtom表現RSS1.0表現も参照できます。

VIASO/K-power追悼

三菱UFJニコスが生まれて、今まで別だったUFJカード・NICOSカード・DCカードのログインポータルがひとつになった。とは言っても単に旧3種と新しいMUFGカードのログイン画面へのリンクや共通キャンペーン情報などがあるだけで、この先には元あったログイン画面が残っている。

私はMUFG系だとニコスのVIASOカードとUFJのK-powerカードを持っていて、どちらも目的を分けてそれなりに使っていた。そのうち私はこう使っているみたいなのを書こうと思っていたのだけど、旧来のカードの新規募集が軒並み終了されてしまった。もう遅いけど、第1回紫蘇カンファレンスで話したことも含めて、これらのカードについて振り返っておこうと思う。

Viasoカード

ニコスが発行しているものとUFJが発行しているものとで、海外旅行保険やポイントの付き方・モールの取り扱いや還元率が若干違うが、以下はニコス版の話。

100円で0.5ptついて12ヶ月で1000pt以上集めると集めたポイント分だけキャッシュバックされる。キャッシュバックされるタイミングを自分で選べないかわりに、1000pt以上あれば付いたポイントが無駄にならない。1年で1000pt集めるのは一見大変そうだが、「楽ペイ」で払うと100円で1ptになる。楽ペイは支払が自動的にリボルディング払いになるというものだが、最初の1ヶ月は金利が付かないので、限度額が10万なら楽ペイの月支払額を10万円にしておけば実質1回払いと変わらない。ETCやケータイ会社からの請求は100円で1ptつき、楽ペイと組み合わせると1.5ptになる。さらに、ショッピングモールが充実していて、たとえばboopleでの決済に+5%のポイントがつく。オリコモールだと「5倍」だけど、オリコはデフォルトで0.5%還元なので2.5%にしかならない。

ということでネット通販をするのに大変良かったのだが、2009年9月に楽ペイでのポイント2倍が廃止された。その旨伝えるハガキに、ちゃっかり退会方法が載っていたのが面白くて、これで退会してしまったらキャッシュバックが受けられない。10ヶ月で238958円このカードで決済して、7581円分のキャッシュバックを受ける権利があるので、それだけもらったら退会しようと思う。還元率は約3.3%になった。

K-powerカード

ドコモ・au・ソフトバンク・ウィルコムからの請求が、合計月1万円を上限に割り引かれるカード。パンフレットには「通話料金」とあるけど、端末の月賦やコンテンツ代金など、ケータイ会社からの請求ならなんでも対象になる。ケータイ会社からの請求が1000円ごとに1pt、その他の請求が1000円ごとに1ptついて、月に合計30pt集めると翌月のケータイ会社からの請求の10%が翌々月に還ってくる。VIASOカードと同様楽ペイで払うとポイントが2倍つく。つまり、最も都合のいい使い方(ケータイ10000円・その他5000円・楽ペイ)をすると、15000円で1000円還元、還元率6.67%となる。

K-powerカードのキャッシュバック率
1ヵ月の合計ショッピングポイントキャッシュバック率
0~29ポイント0%
30~49ポイント10%
50~99ポイント15%
100~149ポイント20%
150~199ポイント30%
200~249ポイント40%
250ポイント以上50%

実際はそんなに上手くいかない。まず、あくまで2種類のポイントを合算して30ptを超える必要があるので、ケータイ9500円・その他5501円だと、合計15000円を超えているからキャッシュバックされると思いがちだが、ケータイ18pt・その他10ptで合計28pt(楽ペイ前提)であり、30ptに満たないので1円も還元されない。

また、キャッシュバック率の決定→キャッシュバックされる金額の決定→実際のキャッシュバックまで2ヶ月のディレイがある。つまり、還元率を高くしようと思ったら、入会した最初の1ヶ月はケータイ代をこれで決済しないで、次の月から10000円を目指していく必要がある。やめるときはケータイ代だけ残し、翌月にキャッシュバックされたのを見て退会する必要がある。3ヶ月15000円を決済して3ヶ月目に1000円キャッシュバックされるので、この3ヶ月だけ見ると還元率は2.2%であり、記事広告だけ見て6%還元! すごい! と思って契約すると肩透かしを食らう。さらに2年目からは年会費1575円が必要になる。年間の還元率を一番都合のいいパターンで計算すると、1年目は最初の2ヶ月はキャッシュバックがないから(1000*10)/(15000*12)で5.6%、2年目からは年会費がかかって(1000*12-1575)/(15000*12)で5.8%となる。

これを去年の5月に契約してこれまで運用した結果、計算を間違えて多めに決済してしまったり逆に決済額が足らなかったりして、8ヶ月で154865円決済して還元額は4197円。還元率はほぼ3.0%になった。広告の数字ほどではないけど、決して悪くない数字だと思う。ケータイ代はもちろんこれで、あとプロバイダ代や電気・ガスなど毎月引き落とされるものを15000円分集めてほかっておけばいいので、そういうデッキを作れればあとは楽。

計算を間違えたというのは、クレジットカードへの請求の上がる日が各社によってまちまちなのを知らなくて、これは足りない!と思って30ptになるように買い物→あとから請求が上がってきて意味がなかったというのがあった。2月10日請求の利用明細を見ると、11月30日のNTT東日本から1月6日の東京電力まで幅広い利用日が並んでいる。NTT東日本の請求の上がるのが遅いからこうなるのだが、使いはじめでそういうのを知らなかったから、未確定請求に東京電力がなくて1ヶ月以上前のNTT東日本からのがあれば東京電力の請求が来月回しになってしまったのではないかと焦るのももっともだと思う。「東京電力からのお知らせ」には、クレジットカード会社の締切日と当社の検針日の関係により、2ヶ月分の電気料金があわせてご請求となる場合がございますと書いてあるし……

K-powerのほうは楽ペイ2倍が廃止されず、あまり考えないでこれだけ還元されるカードもなかなかないと思うので、まだ使い続ける予定。

タグ

年収ジェネレータ

昨晩年収ジェネレータというweb5.0的サービスをリリースしまして、そのきっかけをここにまとめようとしていたのですが有志の方がすでにまとめてくださったので特に書くことがありません。まきもとさんのワンライナーを使っているとfuba様に思われたような感じですが、別の方法で実装しています。本当は平均年収がよく出るような実装にしたら良かったのですが、金額生成部分は1行で書いたので、だいたい0円~3400万円が均等に出るようになっています。確認したところ少ない人で4万円程度、検索結果を眺めると1000万から2000万の人が多い印象です。私の場合約250万だったので、正直それほど高めに出てくるとは思いませんでした……ってアルゴリズムを知っている人が読むと白々しいのですが。

アドバイスをいただいたので途中からTwitterにPOSTするアンカーを設けました。それのおかげか、今日未明から昼過ぎくらいまで割と長い間Buzzっていたようです。ありがたいことです。なにがWeb5.0なのか知りませんが、こういうライトなものがどんどん広まっていく様子を観察できるのもTwitterのいいところですよね。ブログだとこんな1行ネタを脊髄反射で投稿してはい次なんてありえないし、まとめて観察もしにくい。気軽にアウトプットできないからこんなに広がらないし、今まで身内程度でしか面白がれなかったのがどんどんまきこめていける。個人ニュースサイトからソーシャルブックマーク、そしてマイクロブログと情報伝達がどんどんボトムアップ型へシフトしていく中で、そのエキサイティングさとエネルギーを久しぶりに実体験できました。

4万円台をはるか下回る結果が出ました。おめでとうございます(謎

井蛙さんの今年の年収は313円でした。

タグ

ブログから性別を判定する研究とかありましたね

ようざかは名前がちさですし、女の子ですよ

@youzaka さんの性別を疑った事はないです。あとトイレさんも

@okadapan はい……

一方@siro_xxは、先日のチームラボ&pixiv合同会社説明会(便宜的に@tksさんのtweetにリンクを貼っているけど、もっとそれっぽいアンカー先募集)で性別詐称に成功していた。

タグ

アンカー生成をブックマークレットで

たいした話でないばかりに書かずにいたら外で参照できずに残念な思いをしたので悔い改めるシリーズ。

IEだったらコンテクストメニュー追加セットでやっていたようなことをOperaではopera exを使ってしていたけど、いま更新停止中だし、よく考えたらブックマークレットで十分なのでブックマークレットを書いた、というか他にそういうことをやっていた人のを参考にした。

ニックネームアドレス
majavascript:window.prompt('','<a\x20href=\x22'+location.href+'\x22\x20title=\x22'+document.title+'\x22>'+document.title+'</a>')
mljavascript:window.prompt('',document.title+'\x0A'+location.href)
mbjavascript:window.prompt('','<blockquote\x20cite=\x22'+location.href+'\x22\x20title=\x22'+document.title+'\x22>\x0A<p>'+window.getSelection()+'</p>\x0A</blockquote>')
mqjavascript:window.prompt('','<q\x20cite=\x22'+location.href+'\x22\x20title=\x22'+document.title+'\x22>'+window.getSelection()+'</q>')

ということで、ウェブサイトを見ていてアンカーを生成したくなったら、Ctrl+lmaEnterCtrl+xEscAlt+TabCtrl+vな感じ。

ただ、window.getSelection()を使うと選択した箇所が単にテキストとして扱われるので、選択箇所にアンカーなどほかの要素がマークアップされていてもそれが反映できない。コンテクストメニュー追加セットは選択範囲の子要素もうまく扱ってくれていたので、Operaでもそこらへんうまくやれるようなやり方を見つけていきたい。

あとはbはてブにPOSTできるようにしたり、lldRにフィードを投げられるようにしたり、tTumblrにPOSTできるようにしたり (Opera版Tomblooください)、mmitterにPOSTできるようにしたりしている。これらはオフィシャルで配布しているブックマークレット。

タグ

解こう!イラストロジック!

イラロジのセンスが無いだけなのかもしれんけど、これ解くのに3ヶ月かかりました。(DL-PDF 30KB

ということでやってみる。

まず、セオリーに従って数字の大きいところに注目、あとはそこからどんどん論理的に考えて塗ったり×印をつけたりすればいい (図1; 茶色は塗るところ、灰色は×印を打つところ)。

図1 これ以上どうする?

図1まで塗り終わったあと手が止まった。これ以上どこを塗れるだろうか。

手詰まりなので、たとえば、[4 3]の列(左から4番目の列)に注目してみる。この列は、[1 1 1 1 1 2]の行(上から7番目の行)に×があって上下2つにわかれている。上部は6マスあり、ここに[4]が来る可能性がある。一方下部は8マスあり、[4]と[3]が両方塗られる可能性がある。なので、現時点ではどちらが塗られるべきマスか確定しないのだが、とりあえず仮定して塗ってみる (図2、図3)。

図2 上6マスに[4]が入ると仮定
図3 下8マスに[4 3]が入ると仮定

このあと塗りすすめていくと、図3の場合、そのうち破綻してしまう (図4)。

図4 図3の状態から論理的に塗りすすめていく

たとえば、[2 2 1 2]の行(上から1番目の行)は、×が打たれていないマスが[2 1 2 2]となっていて明らかにうまく塗れない。[2 2 2]の列(左から1番目の列)も、空いた4マスに残った[2 2]を塗るのは不可能だ。

ということで、図2の状態に戻して再び考え始める。とはいっても、[3 2 1]の行(上から4番目の行)と[2 2 2]の列(左から1番目の列)の交点に×をつけられるくらいだ (図5)。

図5 図2の状態に戻してまた塗り始める

ただ、ここで×がつけられたのが大きい。いま[2 2 2]の列は3つの×で4つに分割されている。連続したマスは最大でも4であり、分割された空間に[2 2]が入ることはない。つまり、分割された4つの連続したマスのうちどれか3つにそれぞれ[2]が塗られることになる。そこで、分割された一番下に注目する。ここは2マスしか空いていないから、ほかの空間が塗られることを仮定するより多くのマスを塗ることができる (図6)。

図6 左下隅のタテ2マスが塗られると仮定

はたしてこれが大正解で、ここまでくると正解まで一気に塗り進めることができる (図7)

図7 完成

ということで、解いていく中で仮塗りを2回しないといけなかった。

個人的には、仮塗りをせずに解けるほうがイラストロジックとして完成度が高いと思う。消しゴムなしで解きたい。

タグ

金やる

金くれについて 軽犯罪法違反だから閉鎖をお薦めしますというメールが来たらしいので、軽犯罪法に問われなくなるソリューションを考えた。

つまり、「金やる」というサービスを開設する。「金やる」は、金をやりたい人がサイトに載っている口座番号に向けて金をやることができる次世代ネット寄付プラットフォームということにする。これなら件のメールをよこした人も大満足だし、昨今上から目線が大人気なので「金やる」なんてサービス名はヒット間違いなし。ほげ。

タグ

やっと神保町二郎に行けた

二郎の中でも評判の高い神田神保町店へ行ってきた。開店30分前に着いたのにすでに20人も並んでいて、結局食券を買うまで1時間かかった。いくら土曜の朝だからって人大杉で、それだけ本当に評価が高いのだろう。普段行く桜台駅前店では、もうきつくなってきたので小豚も頼んでいないのだけど、今日はせっかくの機会なので小豚を頼んで、ニンニクマシマシヤサイとした

まず、なにより豚が柔らかくてかなり美味い! これはすごい。最近桜台では豚の劣化が気になっていて、久しぶりに二郎らしい豚を食べられたと思った。あと、桜台だと客のコールを訊くのは完全に助手の役割になっていて店主はもっぱらラーメン作りだけど、ここは店主に近い客には店主がコールを訊いていたのが好印象だった。もっとも、桜台のイケメン店主に直接訊かれると客としてもすわりが悪いので、そこは店ごとの空気があると思う。

麺は桜台に比べるとやや細めで、カエシが底に沈んでしまっていて上のほうは味があまりなかったけど、本当においしかったし、やっぱり評判通りで来てよかった。店を出ると、行列は大通りの突き当りまで延びていた。どう見ても40人以上いる。さすがにどうかと思った。

二郎はおいしいけどどうみても太るので、せめて隔週にしようとしているのだけど、二郎に行かない週は大に行ってたり、今月はもう3回も行ってしまっている。毒に中っている。それでも3月の健診からは12kg痩せていてやっとBMIが25を切りそうな感じになってきた。バランスを取りつつおいしくいただいていきたい。

タグ

Favotterにcensoredされた

最近Favotterにcensored機能が実装され、Google AdSenseに怒られるような単語が<censored>に置換されて表示されるようになった。私の場合、いわゆる変態クラスタと呼ばれているような方々がなさっているような偽悪的な投稿は行っておらず、このようなフィルタに引っかかるはずはないと思っていたのだが、LLTVのときに投稿した幼女censoredされてしまっていたのに気づいた。なんということでしょう。いや、よくよく検索してみたら何度もそういう投稿をしていたのでそういう投稿をした自分がアレなんだけど、はじめてFavotter上で自分の投稿がcensoredされたのを見てちょっとショックだった

ところで、件の投稿をふぁぼったーで見ると、「幼女」がcensoredされていません。そりゃあそうですよね。「幼女」がcensoredされたことを嘆いているのに、肝心の「幼女」がcensoredされたらFavotter上で見たときに文意がつかめません。これはふぁぼったーが私のそういう気持ちを酌んでくれている、というわけではなくて数値文字参照で書き込んだからです。変換には「使えない文字」のHTML4文字参照フォームを利用し、これにより「幼女」の数値文字参照である&#24188;&#22899;を得ました。この文字列をTwitterに投稿すると、画面上には「幼女」と表示されますが、データとしてはあくまで"&#24188;&#22899;"なので、Favotterの検閲フィルタを突破できたということです。アドセンスの中の人が数値文字参照まで気にするかどうか知りませんが、もし数値文字参照に置換すればオッケーなのだとしたら、機械も人間もWin-Winですね。

ところで、第9回文学フリマで頒布される『つヤ部報vol2』に、ふぁぼったー <censored> 徹底解説 ―これが卑猥語フィルタだ― @ono_matopeという記事が掲載されているようですね。楽しみです。

タグ

研究室の合宿で全自動メシをクソBotに変換しました

今日まで2日間、研究室の合宿でグランビュー熱海というコンドミニアムで開発合宿をしていた。これがとてもよくて、先生が授業で営業を精力的に行ったためとかでまず女子の割合が高い。そしてご飯が自動的に出てくる。屋上に風呂があって見晴らしがいい。すぐ前が海でリアルが爆発している。そんな中OB共は怠惰な時間をすごしておりまったく救いようがない。

たとえば私などはyozbotというクソみたいなBotを作成するのみで、これは私の今までの投稿をなんかマルコフ連鎖? とかいうので再構築しているもので、ていうかまず実装したアルゴリズムがマルコフ連鎖になっているのかも不明で、ただPythonの勉強をしたかっただけだった。

まず、自分のタイムラインは古い分がすでにサーバにあるので、新しい部分を1時間おきくらいにでも取得してくる必要がある。こんな感じ:

#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import MySQLdb
import twitter
import time
import calendar

api = twitter.Api()
statuses = api.GetUserTimeline('youzaka')

def tosqltime(created_at):
    return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(calendar.timegm(time.strptime(created_at, '%a %b %d %H:%M:%S +0000 %Y'))))

data = [(s.id, s.text.encode('utf_8'), tosqltime(s.created_at)) for s in statuses]

con = MySQLdb.connect(db='*****', user='*****', passwd='*****')
cur = con.cursor()

for (id, body, date) in data:
    sql = "insert ignore into tweets(id, body, date, treated) values(%s, %s, %s, 0)"
    cur.execute(sql, (id, body, date))
cur.close()

タイムライン取得APIを叩いて、IDと本文と日付を取得して、それをDBにつっこんでいるだけ。tweetsテーブルのidカラムは主キーになっているので、IGNOREをつけておく。15行目で、取得したtextをそのまま使っていたらDBにつっこむところでUnicodeEncodeErrorが出た。Pythonはここら辺が慣れない。

マルコフ連鎖みたいなのが以下:

#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import MeCab
import MySQLdb
import re
import random

mecab = MeCab.Tagger('-Owakati')
con = MySQLdb.connect(db='*****', user='*****', passwd='*****')
cur = con.cursor()
sql = 'select id, body from tweets where treated = 0'
cur.execute(sql)
rows = cur.fetchall()
id_list = []
markov = {}

for row in rows:
    id_list.append(row[0])
    body = row[1]
    first = ""
    second = ""
    body = re.sub('https?://[^ ]+', '', body)

    if body == '':
        continue;

    parsed = mecab.parse(body.rstrip()).split(' ')
    parsed.insert(0, '[start]')
    parsed[-1] = '[end]'
    for third in parsed:
        if first and second:
            if (first, second) not in markov:
                markov[(first, second)] = []
            markov[(first, second)].append(third)
        first, second = second, third
cur.close()

cur = con.cursor()
for (first, second), list in markov.items():
    for third in list:
        sql = "insert into markov(first, second, third) values(%s, %s, %s)"
        cur.execute(sql, (first, second, third))
cur.close()

cur = con.cursor()
for id in id_list:
    sql = "update tweets set treated = 1 where id = %s"
    cur.execute(sql, id)
cur.close()

取得したTwitterの書き込みのうち未処理のものについて、URLを削除してMecabに投げて形態素の配列を取得して、先頭と最後にそれぞれ[start]、[end]のマークをつける。で、どんどんDBに投げていく。たとえば、「研究室の合宿で全自動メシをクソBotに変換しました」を形態素分解すると「研究室」「の」「合宿」「で」「全」「自動」「メシ」「を」「クソ」「Bot」「に」「変換」「し」「まし」「た」となる (デフォルトの辞書の場合) ので、この文章から以下のルールを得られる:

startsecondthird
[start]研究
研究
合宿
合宿
合宿
自動
自動メシ
自動メシ
メシクソ
クソBot
クソBot
Bot変換
変換
変換まし
まし
まし[end]

このルールから文章を作成してTwitterに投げるところはこんな感じ:

#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import MySQLdb
import re
import twitter

con = MySQLdb.connect(db='*****', user='*****', passwd='*****')
cur = con.cursor()
sql = "select second, third from markov where first='[start]' order by rand() limit 1"
cur.execute(sql)
row = cur.fetchone()

first = row[0]
second = row[1]
cur.close()

if re.search('[a-zA-Z]$', first) and re.search('^[a-zA-Z]', second):
    text = first + ' ' + second
else:
    text = first + second

while True:
    cur = con.cursor()
    sql = "select third from markov where first = %s and second = %s order by rand() limit 1"
    cur.execute(sql, (first, second))
    row = cur.fetchone()
    first, second = second, row[0]

    if second == '[end]':
        break

    if re.search('[a-zA-Z]$', text) and re.search('^[a-zA-Z]', second):
        text = text + ' ' + second
    else:
        text += second

api = twitter.Api('yozbot', '*****')
api.PostUpdate(text.decode('utf_8'))

まず、最初の2語をランダムに選んで、その2語から次の単語をランダムに繋げていって、[end]が出たら終わりという単純なもの。次の単語を選ぶのに前の2語を使っているので、バリエーションの少ない組み合わせだとそのままもとの投稿が出力されるということもありえて、そこらへんは調整したい。「北大」→「クラスタ」の連なりが1例しかなかったので、「北大」が選択された場合は100%「北大クラスタ爆発しろ」になってしまう。

firstsecondthird
むしろクラスタ
特定クラスタ
所属クラスタ以外
所属クラスタごと
からクラスタ
操作クラスタ[end]
関東クラスタ
関東クラスタ
クラスタ解説
クラスタ
北大クラスタ爆発
クラスタ
クラスタ
クラスタ
筑波大クラスタ
筑波大クラスタよく
ないクラスタ
クラスタ
クラスタより
クラスタっぽい
クラスタ
クラスタ
クラスタ
クラスタ
クラスタ
会話クラスタ
クラスタ
京大クラスタ
クラスタ
働くクラスタ
高専クラスタ?
新人クラスタ

これ、「大」→「クラスタ」ってなっているもののほとんどが「静大クラスタ」だったはずで、「京大」「北大」はあるのに「静大」はなくて辞書漏れ悲しいですという話ですね。

タグ

牛丼キングを食べに行った

噂の牛丼キングを食べに新宿南口のすき家へ行った。新宿駅の南口から出てすき家までの道中に吉野家が2軒もあったのが印象的だった。ていうか「牛丼キングを食べに行く会」ということで3人で行ったはずなのに、結局牛丼キングを頼んだのが私だけだったのがちょっとアレだった。

量はメガ牛丼の2倍、並の6倍ということで、普通に半分まで食べた時点でお腹一杯だし、これ以上同じ味というのもきついという感じになったけど、ご飯はこれを残してはならないと育てられたのでがんばって完食した。食器を下げに来た店員が空の丼を見て笑っていたのがアレだった。よもや完食しまいと思っていたな。

関係ないんだけど、近所のラーメン大は野菜の量が「少し多め」、「多め」、「増し」などとあって「マシマシ」は明示されていない。増しでも二郎のそれより十分多いのだけど、マシマシにすると野菜がこれの2倍来る。当然同じ丼には乗らないので、ラーメン+増し分の野菜が乗ったものと野菜だけが乗ったものの2つが供される。あのときは、店員から何も知らずに二郎のつもりでマシマシを頼みやがってという空気を感じたので、当然のように全部食べた。残したら負けだし、そしたら恥ずかしくて大に行けなくなってしまう。隣の人は、メニューにあるのはあくまで「増し」までなので「増し」を頼むとああなるのかと勘違いしたみたいで「少し多め」にしていた。すみません。二郎に比べて丼が小さいし麺も細いので、意外と無理なレベルではない。さすがに昴神角ふじの野菜本増しとか麺香房暖々の野菜多めのレベルになるとごめんなさいだけど。

タグ

光が丘公園でジョギングしている

最近は週末に光が丘公園ジョギングコースでジョギングするようにしている。だいたい3kmのコースになっていて、ヘタレなので20分弱で1周して40分強休憩するみたいなペースで3周くらい走って帰る感じ。コースは緑が豊かで、走っていて気持ちいい。途中バーベキュー場の横を通るところがあって、そこに差し掛かると心が折れそうになる。

タグ

行列をスキップできる権利

らーめん大練馬店が開店して、通常600円のラーメンが100円で食べられるとのことでアホみたいな行列になっている。開店直後でも80人以上並んでいたりする。そりゃ100円は安いけど、高々500円引きですよ。吉野家コピペでは150円引きのために普段来ていない吉野家に家族連れで来ている半可通が揶揄されているが、500円のために数時間並ぶというのも同様に揶揄されてしかるべきだ。

たとえば、行列をスキップしていきなり専用席に座れる権利を500円で用意して、最後尾グループに数枚配布するとする。この権利は自由に売買できて、値段も自由に決められるとする。どう値段が変化してどう売買されるのだろうか。

まず、このチケットを受け取った人は、そもそもラーメンを食べに来ているので早速権利を行使するだろう。しかし、周りの人も同じくラーメンが食べたいはずなので、もしかしたら600円とか1000円とかで買いたいと思うかもしれない。でも良く考えるとそれ買っちゃうと定価より高いし、かといって何時間も並ぶのやだし…みたいな葛藤が飛び交うようになって、観察する分にはおもしろいかもしれない。そこから適切な値段とかも見えてくるだろうしね。実際どうなるんだろう。

タグ

普通にphp-mysqliを使ってCRUDするコード例

普通にMysqliを使ってみます。特別なネタがあるわけではありません。リファレンスが分かりにくいという声を近くで聞いたので、まあだいたいこんな風にすれば良いんじゃない? というのをメモとして残しておきます。

個人ユースのデータベースといえばやはり住所録ということで、これにデータを出し入れする部分だけMysqliを使って書いてみます。住所録のデータスキームはこんな感じ:

create table addresses(
    id int unsigned not null primary key auto_increment,
    name varchar(32) not null,
    kana varchar(64) not null,
    zipcode char(8) not null,
    address1 varchar(64) not null,
    address2 varchar(64) not null,
    tel char(13) not null,
    email varchar(255) not null,
    index(kana)
);

IDを指定してデータを取得する関数は、だいたいこんな感じに書ける (実際はもっとまじめにエラー処理とかすべき):

// インスタンス変数として$con = new Mysqli("ホスト名", "ユーザ名", "パスワード", "データベース名");が存在することを前提としている。以下同様
function getAddressById($id) {
    $result = array();
    $sql = "select name, kana, zipcode, address1, address2, tel, email from addresses where id = ?";
    $stmt = $this->con->prepare($sql);
    $stmt->bind_param("i", $id);
    $stmt->execute();
    $stmt->bind_result($name, $kana, $zipcode, $address1, $address2, $tel, $email);
    if($stmt->fetch()) {
        $result = compact("id", "name", "kana", "zipcode", "address1", "address2", "tel", "email");
    }
    $stmt->close();
    return $result;
}

1ページに20件表示させるとして、その20件分を持ってくる関数はこんな感じに書ける:

// $page: ページ数。最初のページは1
function getAddresses($page, $limit = 20) {
    $result = array();
    $sql = "select id, name, kana, zipcode, address1, address2, tel, email from addresses order by kana limit ?, ?";
    $offset = ($page - 1) * $limit;
    $stmt = $this->con->prepare($sql);
    $stmt->bind_param("ii", $offset, $limit);
    $stmt->execute();
    $stmt->bind_result($id, $name, $kana, $zipcode, $address1, $address2, $tel, $email);
    while($stmt->fetch()) {
        $result[] = compact("id", "name", "kana", "zipcode", "address1", "address2", "tel", "email");
    }
    $stmt->close();
    return $result;
}

指定したカナに前方一致するものを取得したいならこんな感じ:

function getAddressesByKana($kana, $page, $limit = 20) {
    $result = array();
    $sql = "select id, name, kana, zipcode, address1, address2, tel, email from addresses where kana like ? order by kana limit ?, ?";
    $offset = ($page - 1) * $limit;
    $like = "{$kana}%";
    $stmt = $this->con->prepare($sql);
    $stmt->bind_param("sii", $like, $offset, $limit);
    $stmt->execute();
    $stmt->bind_result($id, $name, $kana, $zipcode, $address1, $address2, $tel, $email);
    while($stmt->fetch()) {
        $result[] = compact("id", "name", "kana", "zipcode", "address1", "address2", "tel", "email");
    }
    $stmt->close();
    return $result;
}

新しいエントリを挿入するのはこんな感じ:

// $addressはnameやkanaなどのキーがある連想配列
function insert($address) {
    $result = 0;
    $sql = "insert into addresses(name, kana, zipcode, address1, address2, tel, email) values(?,?,?,?,?,?,?)";
    $stmt = $this->con->prepare($sql);
    $stmt->bind_param("sssssss", $name, $kana, $zipcode, $address1, $address2, $tel, $email);
    extract($address, EXTR_OVERWRITE);
    $stmt->execute();
    $result = $this->con->insert_id;
    $stmt->close();
    return $result;
}

まとめて挿入するなら:

// $addressesはnameやkanaなどのキーがある連想配列の配列
function insertAll($addresses) {
    $result = array();
    $sql = "insert into addresses(name, kana, zipcode, address1, address2, tel, email) values(?,?,?,?,?,?,?)";
    $stmt = $this->con->prepare($sql);
    $stmt->bind_param("sssssss", $name, $kana, $zipcode, $address1, $address2, $tel, $email);
    foreach($addresses as $address) {
        extract($address, EXTR_OVERWRITE);
        $stmt->execute();
        $result[] = $this->con->insert_id;
    }
    $stmt->close();
    return $result;
}

更新するなら:

function update($address) {
    $sql = "update addresses set name = ?, kana = ?, zipcode = ?, address1 = ?, address2 = ?, tel = ?, email = ? where id = ?";
    $stmt = $this->con->prepare($sql);
    $stmt->bind_param("sssssssi", $name, $kana, $zipcode, $address1, $address2, $tel, $email, $id);
    extract($address, EXTR_OVERWRITE);
    $stmt->execute();
    $stmt->close();
}

ついでに削除:

function delete($id) {
    $sql = "delete from addresses where id = ?";
    $stmt = $this->con->prepare($sql);
    $stmt->bind_param("i", $id);
    $stmt->execute();
    $stmt->close();
}

function deleteAll($id_list) {
    $sql = "delete from addresses where id = ?";
    $stmt = $this->con->prepare($sql);
    $stmt->bind_param("i", $id);
    foreach($id_list as $id) {
        $stmt->execute();
    }
    $stmt->close();
}

実際はフレームワークとかでささっとやってしまう部分かもしれませんが、あえてフルスクラッチするならこんな感じでしょうか。

タグ

Yahoo Widgetで発車時刻表ウィジェットを作ってみた

最近まわりでYahoo!ウィジェットで何か作るのがブームなので、私もそれに乗って作ってみた。id:n_shuyoid:BigFatCatid:ksmemoの各氏によるメモを見ればだいたい分かる感じ。関係ないけどはてなダイアリーってこういう検索結果を日時の昇順で見ることはできないのだろうか。

XMLでUI仕様を書いて、動作をJavaScriptで書いていく。デバッガがついているので、まあそれで試行錯誤していけばいいのかも。JavaScriptは仕様と違う動作が行われるとかはないけど、WindowsオブジェクトがなくてsetTimeoutとかができないので、自動に表示が変わるみたいなのをするには一手間必要。あとリファレンスが大量すぎて読み通す気力が起きなかったりチュートリアルと違うことが書いてあったりするけど、そこはがんばる。

で、せっかくのWidgetで、やっぱWidgetといえば画面常駐がキモで、ブラウザを開いてみればいいようなものはなるべく避けたい。あとインターネットとやりとりできるのを活かしたい。たとえば、書いた文書が(明示的にEnterなどを押さなくても)そのままインターネットに大公開されるようなテキストエディタだかWikiクライアントだかわからないようなものなどはぜひこのYahoo Widgetで作るべきだろう。しかし、今回は諸般の事情でPOSTできるようなものは歓迎されないので、GETに徹したものを作ることにする。

ということで、goo時刻表をスクレイピングして現在の発車時刻表をそれっぽく表示するウィジェット (拡張子を.widgetに変えてウィジェットエンジンにつっこめば動くはず) を作った。画面イメージは以下のような感じ:

駅名を入力して路線と方面を選ぶと、右側に現時点での発車時刻が表示される。表示は1分毎に自動的にリロードされる。できれば運行情報が下のほうでスクロールしていたりパタパタみたいなエフェクトをかけたかったけどそこまでできなかった。あと、左ペインがマウスホイールでスクロールしてほしかったんだけど、なぜかそれもできなかった。将来的にこれのiPhone版も作りたい。

ソース

traindeps.kon

<?xml version="1.0" encoding="UTF-8"?>
<widget minimumVersion="4.0">
    <settings>
        <setting name="debug" value="off" />
    </settings>
    <window name="main" width="180" height="30" style="background:#333;">
        <title>時刻表表示機</title>
        <text name="label" color="#fff" alignment="left" style="font-size:12px">
            <hOffset>10</hOffset>
            <vOffset>20</vOffset>
            <data>駅名</data>
        </text>
        <textarea name="data" columns="14" lines="1" style="background:#fff;color:#999">
            <hOffset>40</hOffset>
            <vOffset>7</vOffset>
            <data>駅名を入力してください</data>
        </textarea>
        <frame name="mframe" width="150" height="115" vLineSize="10" vScrollBar="mainv">
            <hOffset>7</hOffset>
            <vOffset>30</vOffset>
            <vScrollBar>mainv</vScrollBar>
        </frame>
        <scrollbar name="mainv" height="115" width="15" autoHide="true" thumbcolor="#f00">
            <hOffset>160</hOffset>
            <vOffset>30</vOffset>
        </scrollbar>
        <text name="disptime" color="#fff">
            <hOffset>200</hOffset>
            <vOffset>20</vOffset>
            <data></data>
        </text>
        <text name="time" color="#fff" alignment="left" style="font-size:12px">
            <hOffset>200</hOffset>
            <vOffset>45</vOffset>
            <data>発車</data>
        </text>
        <text name="type" color="#fff" alignment="left" style="font-size:12px">
            <hOffset>240</hOffset>
            <vOffset>45</vOffset>
            <data>種別</data>
        </text>
        <text name="deps" color="#fff" alignment="left" style="font-size:12px">
            <hOffset>300</hOffset>
            <vOffset>45</vOffset>
            <data>行先</data>
        </text>
        <frame name="tframe" width="300" height="120">
            <hOffset>200</hOffset>
            <vOffset>55</vOffset>
            <text name="status" color="#fc0">
                <hOffset>0</hOffset>
                <vOffset>15</vOffset>
                <data></data>
            </text>
        </frame>
        
        <script src="traindeps.js" charset="UTF-8" />
    </window>
</widget>

traindeps.js

hogeという名前の関数オブジェクトがあったりしてひどい。

(function() {
items = {};
deps = {};
types = {};
prev = "";
done = false;

var reset = function() {
    items = {};
    deps = {};
    types = {};
    prev = "";
    done = false;
    main.width = 180;
}
var display = function(count) {
    if(done) {
        main.width = 450;
        
        var now = new Date();
        var hour = now.getHours() < 3 ? now.getHours() + 24 : now.getHours();
        var minute = now.getMinutes();
        var current = (hour < 10 ? "0"+hour : hour) + "" + (minute < 10 ? "0"+minute : minute);
        if(current == prev) return;
        
        disptime.data = hour+"時"+minute+"分現在";
        if(tframe.hasChildNodes()) {
            var children = tframe.subviews;
            for(i=0; i<children.length; i++) {
                tframe.removeChild(children[i]);
            }
        }
        var i = 0;
        var v = 15;
        var tomorrow = false;
        for(var j in items) {
            
            if(j < current && !tomorrow){
                delete items[j];
            } else {
                tomorrow = true;
                var line_time = new Text();
                line_time.hOffset = 0;
                line_time.vOffset = v;
                line_time.color = "#ffcc00";
                
                line_time.data = j.substr(0,2)+":"+j.substr(2,2);
                tframe.addSubview(line_time);
                
                var type = "普通";
                if(items[j].type in types) type = types[items[j].type];
                var line_type = new Text();
                line_type.hOffset = 40;
                line_type.vOffset = v;
                line_type.data = type;
                
                if(type.indexOf('特') > -1 || type.indexOf("ラ") > -1)
                    line_type.color = "#ff9999";
                else if(type.indexOf("準") > -1)
                    line_type.color = "#ccff99";
                else if(type.indexOf("区") > -1)
                    line_type.color = "#99ff99";
                else if(type.indexOf("勤") > -1)
                    line_type.color = "#99ffff";
                else if(type.indexOf("新") > -1)
                    line_type.color = "#ffcccc";
                else if(type.indexOf("快") > -1)
                    line_type.color = "#cc99ff";
                else if(type.indexOf("急") > -1)
                    line_type.color = "#9999ff";
                else line_type.color = "#ffcc00";
                
                tframe.addSubview(line_type);
                
                var line_deps = new Text();
                line_deps.hOffset = 100;
                line_deps.vOffset = v;
                line_deps.data = (deps[items[j].dep] || items[j].dep);
                line_deps.color = "#ffcc00";
                tframe.addSubview(line_deps);
                
                var line = j + " " + (deps[items[j].dep] || items[j].dep);
                if(items[j].type in types)
                    line += (" " + types[items[j].type]);
                //print(line);
                i++;
                v += 15;
            }
            if(i == count) break;
        }
        if(v == 15) {
            var status = new Text();
            status.color = "#ffcc00";
            status.data = "本日の運行は終了しました。";
            status.hOffset = 0;
            status.vOffset = v;
            tframe.addSubview(status);
        }
        prev = current;
    }
}
var setTimeout = function(func, delay) {
    var timer = new Timer();
    timer.interval = delay / 1000;
    timer.onTimerFired = function() {
        if((typeof func) == "function")
            func();
    else eval(func);
    }
    timer.ticking = true;
    return timer;
}

var timer3 = setTimeout("display(5)", 1000);

data.onGainFocus = function() {
    this.data = "";
    this.style.color = "#000";
};

var clearInterval = function(timer) {
    timer.ticking = false;
}
var getTimetable = function(path) {
    reset();
    var url = "http://transit.goo.ne.jp" + path;
    
    if(!done) {
        var req = new XMLHttpRequest();
        main.width = 450;
        
        if(tframe.hasChildNodes()) {
            var children = tframe.subviews;
            for(i=0; i<children.length; i++) {
                tframe.removeChild(children[i]);
            }
        }
        
        status.data = "データ取得中";
        status.color = "#ffcc00";
        tframe.addSubview(status);
        disptime.data = "  時  分現在";
        
        req.onreadystatechange = function() {
            if(this.readyState == 4) {
                var text = req.responseText;
                var hour;
                var tags = text.match(/<li><font[\s\S]+?<\/li>|<th>\s*\d+\s*<\/th>|<div class="fs12">[\s\S]+?<\/div>/g);
                if(tags) {
                    for(var i in tags) {
                        var each = tags[i].replace(/[ \t\n\r]+/g, "");
                        switch(each[1]) {
                        case 't' :
                            hour = each.match(/<th>\s*(\d+)\s*<\/th>/)[1];
                            hour = hour < 3 ? parseInt(hour) + 24 : hour;
                            //print(hour);
                            if(hour.length == 1) hour = "0"+hour;
                            break;
                        case 'l' :
                            var m = each.match(/<li><font(color="(.+?)")?><span>(.+?)<\/span><\/font><br><font.*>(.+?)<\/font><\/li>/);
                            if(m[4].length == 1) m[4] = "0"+m[4];
                            items[hour+m[4]] = {dep: m[3].replace(/<.+?>/,''), type: m[2]};
                            break;
                        case 'd' :
                            var pair;
                            while((pair = (/<fontcolor="(.+?)">(.+?)<\/font>/g).exec(each)) != null) {
                                types[pair[1]] = pair[2];
                            }
                            while((pair = (/(\S+?)=(.+?)行( |[\r\n]|<)/g).exec(each)) != null) {
                                deps[pair[1]] = pair[2];
                            }
                        }
                    }
                    done = true;
                    display(5);
                } else {
                    status.data = "指定した時刻表データがありませんでした。";
                }
            }
        }
        req.open("GET", url, true);
        req.send();
    }
}
var hoge = function(e, path, i) {
    e.onClick = function() {
        getTimetable(path);
        if(mframe.hasChildNodes()) {
            var children = mframe.subviews;
            for(i=0; i<children.length; i++) {
                if(children[i].hOffset == 15)
                    children[i].color = "#CCCCFF";
            }
            e.color = "#FFCCCC";
        }
    }
}
var searchStation = function() {
    reset();
    main.height = 30;
    if(mframe.hasChildNodes()) {
        var children = mframe.subviews;
        for(i=0; i<children.length; i++) {
            mframe.removeChild(children[i]);
        }
    }
    var url = "http://transit.goo.ne.jp/tconfirm.php?an=7&st_name=" + data.data;
    var req = new XMLHttpRequest();
    req.onreadystatechange = function() {
        if(this.readyState == 4) {
            var text = req.responseText;
            
            var stations = text.match(/<dt><a href="\/timetable\/.+?">.+?<\/a><\/dt>|<span><a href="\/timetable\/.+?">\[&nbsp;.+方面&nbsp;\]<\/a><\/span>/g);
            
            var v = 0;
            for(i = 0; i < stations.length; i++) {
                station = stations[i];
                var line = new Text();
                line.id = "cand"+i;
                
                if(/^<dt>/.test(station)) {
                    line.hOffset = 2;
                    line.color = "#CCCCCC";
                } else {
                    line.hOffset = 15;
                    line.color = "#CCCCFF";
                    var anchor = station.match(/^.+?"(.+?)".+$/)[1];
                    
                    hoge(line, anchor, i);
                }
                line.vOffset = v = (v + 15);
                line.data = stations[i].replace(/<.+?>|[\[\]&nbsp;]/g,'');
                mframe.addSubview(line);
            }
            main.height = 150;
        }
    };
    req.open("GET", url, true);
    req.send();
};

label.onClick = searchStation;
data.onKeyPress = function() {
    if(system.event.keyString == "Return" || system.event.keyString == "Enter") {
        searchStation();
        //getTimetable("");
    }
}
})();
タグ

フォーカスを当てるとデフォルト文字列が消えるinput text (jQuery編)

いろんな文献を気軽に参照しにくい環境になり、いちいち検索しなくてもいいようにここにまとめておきたい感じになったのでしばらくそんな普段書かないようなPOSTばかりします。

まず、最近良くある、デフォルトの状態では入力例を表示して、フォーカスを合わせると消えるというインプットボックスの実装例について。このブログのコメント入力フォームがまさにそんな感じ。

HTML:
<form action="hoge.php" method="get">
<p><input type="text" name="q" id="q" size="30" value="検索語を入力してください"><input type="submit" value="検索"></p>
</form>
JavaScript(jQuery):
$(function(){
    $("#q").focus(function() {
        if($(this).val() == $(this).attr('defaultValue'))
            $(this).val('');
    }).blur(function() {
        if(jQuery.trim($(this).val()) == "") {
            $(this).val($(this).attr('defaultValue'));
        }
    });
});

$(this) を3度実行するのは勿体無いので最初に var $this = $(this) して $this を使い回すとか。/ もしくは if ( this.value == this.defaultValue ) this.value = '' でもよさそう。

確かに、このthisはわざわざjQueryオブジェクトにしなくても、そのままDOMのプロパティを参照すれば用が足りますね。このネタそのものが、単体だとわざわざjQueryを使うまでもないレベルですけど。

実例:

フォーカスがあたったときにvalue属性の値がdefaultvalueだったら消して、逆にフォーカスが外れたときに値が空文字列だったらdefaultvalueを表示するというだけ。このdefaultvalueという属性、どうやら仕様書のいうinitial valueを取得するためにW3C DOMのHTMLInputElementで定義されているもので、ちゃんとW3C DOMを実装しているブラウザならちゃんと使えそう。

デフォルト文字を薄くして実際に入力させるときは黒にしたいというときは、あらかじめCSSで#q{color: #999;}などとした上で、

$(function(){
    $("#q").focus(function() {
        if($(this).val() == $(this).attr('defaultValue'))
            $(this).css('color', '#000').val('');
    }).blur(function() {
        if(jQuery.trim($(this).val()) == "") {
            $(this).css('color', '#999').val($(this).attr('defaultValue'));
        }
    });
});

みたいにしたらいいと思う。

あと、Eventを指定するときに、clickとかkeypressみたいなデバイスに依存するものはなるべく避けてfocusやblurを使うべきだってばっちゃが言ってた。

タグ
© 2001-2010 Chisa YOUZAKA. Some rights reserved.