人生シーケンスブレイク

シーケンスブレイク(Sequence breaking、シークエンスブレイクとも)とは、テレビゲームにおいて開発が想定している攻略ルートを逸脱し、ショートカットする行為のことである。

jQuery Validation Plugin の重複チェックバリデータを作った。

jQuery Validation Plugin で複数のセレクトボックスから項目を選択する場合に、重複チェックするバリデータを作成した。 用途としては、例えば

  • 秘密の質問 Q1〜Q3で同じ質問を選択してないか
  • 好きな○○で重複した項目を選択していないか
  • 経験のある言語で重複した項目を選択していないか

などに利用できるかと思う。

gist.github.com

無駄な空行を一発で削除する

無駄な空行が多いコードから、一発で無駄な部分を消したくなった。

Pythonのflake8をはじめ、他言語のコーディング規約をみても恐らく3行以上の空白を意図的に入れる必要性が感じられないので、4つ以上 <LF> が続いた場合には消す処理で良さそうだ。

command! DeleteUselessBlankLines :%s/\n\{4,}/\r\r\r/

これで :DeleteUselessBlankLines すると3行以上の無駄な空行を削除できるようになる。

もし保存時に自動実行したい場合には、

autocmd BufWritePre * :%s/\n\{4,}/\r\r\r/

とするといいだろう。

Pythonのパッケージ管理ファイル

pip freezeを使う。virtualenv と組み合わせて使うとよい。

$ pip freeze
beautifulsoup4==4.4.1
flake8==2.4.1
mecab-python3==0.7
pep8==1.5.7
pyflakes==0.8.1
PyYAML==3.11
$ pip freeze > requirements.txt  # 現在インストールしているパッケージ一覧をrequirements.txtに書き出し
$ pip install -r requirements.txt  # requirements.txt内のパッケージをインストール

User Guide — pip 7.1.2 documentation

jQuery Validation Plugin のエラーを Bootstrap3 の popover に出力させる。

HTML5 Form Validationのブラウザごとの実装状況が異なることから、 jQuery Validation Plugin | Form validation with jQuery をまだ現役で利用しているケースも多いはず。
先日 jQuery Validation Plugin のエラーを Bootstrap3 の popover(tooltip) に組み込んでみたのでその方法を記録。

実装イメージ。

準備

こんな感じの構成とする

      <form>
        <div class="form-group">
          <label for="exampleInputEmail1">Email address</label>
          <input type="email" class="form-control" name="exampleInputEmail1" placeholder="Email">
        </div>
        <div class="form-group">
          <label for="exampleInputPassword1">Password</label>
          <input type="password" class="form-control" name="exampleInputPassword1" placeholder="Password">
        </div>
        <div class="form-group">
          <label for="exampleInputFile">File input</label>
          <input type="file" id="exampleInputFile">
          <p class="help-block">Example block-level help text here.</p>
        </div>
        <div class="checkbox">
          <label>
            <input type="checkbox"> Check me out
          </label>
        </div>
        <button type="submit" class="btn btn-default">Submit</button>
      </form>

jQuery Validation Plugin の showErrors()メソッドを使う

showErrors()というメソッドがある。
これはエラー時の表示処理を制御するメソッドで、defaultでは以下のようなdefaultShowErrors()メソッドが実行される。

       defaultShowErrors: function() {
            var i, elements, error;
            for ( i = 0; this.errorList[ i ]; i++ ) {
                error = this.errorList[ i ];
                if ( this.settings.highlight ) {
                    this.settings.highlight.call( this, error.element, this.settings.errorClass, this.settings.validClass );
                }
                this.showLabel( error.element, error.message );
            }
            if ( this.errorList.length ) {
                this.toShow = this.toShow.add( this.containers );
            }
            if ( this.settings.success ) {
                for ( i = 0; this.successList[ i ]; i++ ) {
                    this.showLabel( this.successList[ i ] );
                }
            }
            if ( this.settings.unhighlight ) {
                for ( i = 0, elements = this.validElements(); elements[ i ]; i++ ) {
                    this.settings.unhighlight.call( this, elements[ i ], this.settings.errorClass, this.settings.validClass );
                }
            }
            this.toHide = this.toHide.not( this.toShow );
            this.hideErrors();
            this.addWrapper( this.toShow ).show();
        },

端的に言うと、エラー箇所の次の位置にエラーメッセージを含むlabel要素をappendしている。
このshowErrorsメソッドは、以下のような記述でオーバーライド可能。 Validator.showErrors() | jQuery Validation Plugin

$("form").validate({
  rules: {
    exampleInputEmail1: {
      required: true
    },
    exampleInputPassword1: {
      required: true
    }
  },

  showErrors: function(errorMap, errorList) {
    // validationに引っかからなかった要素はpopover非表示にする
    $.each(this.successList, function(index, value) {
      $(value).popover('hide');
    });
    
    // validationに引っかかった要素はpopover表示する
    $.each(errorList, function(index, value) {
      var _popover = $(value.element).popover({
        trigger: 'manual',
        placement: 'bottom',
        content: value.message,
        template: "<div class='popover' role='tooltip'><div class='arrow'></div><h3 class='popover-title'></h3><div class='popover-content'></div></div>"
      });
      _popover.data('bs.popover').options.content = value.message; // popover要素のテキストを更新する
      $(value.element).popover('show');
    });
  }
});

上記を実装するとこんな感じになる。

エラーメッセージなので警告色に

.popover-validation などとclass名を追加して、css

.popover.popover-validation {
  background-color: #f9f2f4;
  color: #c7254e;
}
.popover.popover-validation > .arrow:after {
  border-bottom-color: #f9f2f4;
}

とするとさらにいい感じになった。

showErrors()処理を使いまわしたい

setDefaults()メソッドを利用しましょう。 jQuery.validator.setDefaults() | jQuery Validation Plugin

$.validator.setDefaults({
  // エラーメッセージのBootstrap3: popover表示
  showErrors: function(errorMap, errorList) {
       ...
    });
  }
});

messages: {} も纏めて jquery.validate.settings.js などとして置いとくと、サイト内で共通のエラーメッセージを使い回せます。

纏め

最終的な今回の成果物として、デモ用のリポジトリを用意したのでご参考にどうぞ。

github.com

参考: How to use Twitter Bootstrap popovers for jQuery validation notifications? - Stack Overflow

JavaScript 第6版

JavaScript 第6版

Pythonで破壊的ループをする際はリスト全体のコピーをとる

Pythonで以下のような破壊的ループをしようとすると、indexのズレが発生してすべての要素に対して処理が行われないケースがある。
(この例の処理ではリスト内包表記で充分代替可能だったりするが、あくまで例として単一処理にしている。)

li = [
    {
        "id": "D028xxxxx",
        "is_im": True,
        "user": "USLACKBOT",
        "created": 1397471294,
        "is_user_deleted": False
    },
    {
        "id": "D028xxxxx",
        "is_im": True,
        "user": "U028xxxxx",
        "created": 1397471294,
        "is_user_deleted": False
    },
    {
        "id": "D028xxxxx",
        "is_im": False,
        "user": "U028xxxxx",
        "created": 1397471294,
        "is_user_deleted": False
    }
]

for user in li:
    if user['is_im'] is True:
        li.remove(user)

print(li) # [{'id': 'D028QH1PT', 'created': 1397471294, 'user': 'U028NTG5T', 'is_im': True, 'is_user_deleted': False}, {'id': 'D028QH1PR', 'created': 1397471294, 'user': 'U028P546G', 'is_im': False, 'is_user_deleted': False}]
# 'is_im': Trueの要素が残っている

リスト全体のスライスコピーを取る

正しく破壊的ループをする場合には、リスト全体をコピーして実行すると良い。
ループ処理前に temp_li = li を行うやり方もWebでは散見されたが、
li[:] によるスライスコピーが一番綺麗に記述できる。

for user in li[:]:
    if user['is_im'] is True:
        li.remove(user)

print(li) # [{'created': 1397471294, 'id': 'D028QH1PR', 'is_im': False, 'is_user_deleted': False, 'user': 'U028P546G'}]

filter関数を使う

ちなみにfilter()で書くとこう。

li = list(filter(lambda user: user['is_im'] is False, li))

print(li) # [{'created': 1397471294, 'id': 'D028QH1PR', 'is_im': False, 'is_user_deleted': False, 'user': 'U028P546G'}]

Python3からはfilter()はリストでなくイテレータを返却するようになったので、list()で囲う必要がある。

リスト内包表記を使う

結局はリスト内包表記がいいのかも知れない。

li = [user for user in li if user['is_im'] is False]

print(li) # [{'created': 1397471294, 'id': 'D028QH1PR', 'is_im': False, 'is_user_deleted': False, 'user': 'U028P546G'}]

上記の内容はPythonチュートリアルに記載されていた。数年ぶりに読み返したが、まだ発見があったのでまだまだだなと痛感。

Pythonチュートリアル 第2版

Pythonチュートリアル 第2版

一応速度も計測してみたが、今回のリストだと速度に顕著な差異が見られないので割愛した。 コードはPython3.xに準拠。

Pythonの引数のデフォルト値は一度しか評価されない

先日ハマったのでメモ。

結論

Pythonの引数のデフォルト値は一度しか評価されない。 def func(url, l=[]): … としたい場合には、代わりに

def func(url, l=None):
    if l is None:
        l = []
    …

とする。

背景

GitHub APIを叩いて全てのbranch_listを取得しようと、 以下のような関数を書いた。

def fetch_all(url, all_list=[]):
    res = urllib.request.urlopen(url)
    res_link, res_body = res.getheader('Link'), res.read().decode('utf-8')
    all_list += json.loads(res_body)

    if 'rel="next"' in res_link:
        next_url = res_link.split('; rel="next"')[0].strip('<>')
        fetch_all(next_url, all_list)

    return all_list

が、2度目の fetch_all(url) 実行時には all_listが [] でなく、1度めに実行した際の配列が格納されていることに気付いた。

Pythonチュートリアルを読んでみた

Pythonチュートリアルに以下のように記載されていた。

重要な警告: デフォルト値は一度しか評価されない。デフォルト値が可変オブジェクト、すなわちリスト、ディクショナリ、およびほとんどのクラスのインスタンスであると、このことが影響する。

Pythonチュートリアル 第2版

Pythonチュートリアル 第2版

def f(a, L=[]):
    L.append(a)
    return L

print f(1) # [1]
print f(2) # [1, 2]
print f(3) # [1, 2, 3]

と実行される。 デフォルト値を共有されたくないのであれば、以下のように書く必要がある。

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

print f(1) # [1]
print f(2) # [2]
print f(3) # [3]

冒頭の例だと以下のように書き換えると問題なく動作する

def fetch_all(url, all_list=None):
    # init
    if all_list is None:
        all_list = []

    res = urllib.request.urlopen(url)
    res_link, res_body = res.getheader('Link'), res.read().decode('utf-8')
    all_list += json.loads(res_body)

    if 'rel="next"' in res_link:
        next_url = res_link.split('; rel="next"')[0].strip('<>')
        fetch_all(next_url, all_list)

    return all_list

なんで一度しか評価されないの?

programmers.stackexchange.com

によると、 def func(item, l = something.defaultList()) のように、関数の実行結果をデフォルト引数とした際に都度実行されることを防ぐ為の目的もあるようだ。

tmux 2.1からmouse系のオプションが変更になった。

久しぶりに tmux を再起動したらこんな警告が出るようになった。

/Users/user_name/.tmux.conf:16: unknown option: mode-mouse
/Users/user_name/.tmux.conf:17: unknown option: mouse-resize-pane
/Users/user_name/.tmux.conf:18: unknown option: mouse-select-pane
/Users/user_name/.tmux.conf:19: unknown option: mouse-select-window

tmux 2.1からmouse系のオプションが1つに纏められた模様。

Incompatible Changes
====================

* Mouse-mode has been rewritten.  There's now no longer options for:
    - mouse-resize-pane
    - mouse-select-pane
    - mouse-select-window
    - mode-mouse

  Instead there is just one option:  'mouse' which turns on mouse support
  entirely.

https://raw.githubusercontent.com/tmux/tmux/master/CHANGES

上記4つのオプションを消して、

set -g mouse on

を追加したら問題なく動作するようになった。

2015-11-28 追記

スクロール時に自動でコピーモードにならなくなっていた。 GitHubでもissueがあがっていて、そこに解決方法が載っていた。

bind -n WheelUpPane if-shell -F -t = "#{mouse_any_flag}" "send-keys -M" "if -Ft= '#{pane_in_mode}' 'send-keys -M' 'select-pane -t=; copy-mode -e; send-keys -M'"
bind -n WheelDownPane select-pane -t= \; send-keys -M

Mouse scrolling in tmux 2.1 on OSX no longer auto-starts · Issue #145 · tmux/tmux · GitHub

上記2行で、

  • スクロールアップするとコピーモードに入る
  • 最後までスクロールダウンするとコピーモードを抜ける

が動作するようになる。
ちょっと手元の環境ではtmuxを再起動しても反映されず、tmuxのプロセスを直接killしてから立ち上げてやっと有効になった。