UTF-8 文字列を省略して一定の表示幅に収める
つまり Go の go/runewidth の runewidth.Truncate みたいなことをしたい。
GitHub - mattn/go-runewidth
https://github.com/mattn/go-runewidth
で、どうやら unicode-display_width という gem があるらしい。
GitHub - janlelis/unicode-display_width: Monospace Unicode character width in Ruby
https://github.com/janlelis/unicode-display_width
道具は揃ったので、順当に実装してみる。
require 'unicode/display_width' def truncate_string(str, len, omission) return str.dup unless Unicode::DisplayWidth.of(str) > len lim = len - Unicode::DisplayWidth.of(omission) n = 0 chars = str.each_char.take_while{|c| (n += Unicode::DisplayWidth.of(c)) <= lim } chars.join << omission end
ここでちょっと欲が出た。
Unicode::DisplayWidth.of(str) == str.each_char.inject(0){|r, c| r += Unicode::DisplayWidth.of(c) }
のはずである。
すると、実質的に str に対して二回 Unicode::DisplayWidth.of が呼ばれているってことになるな、と考える。
これ省けばそっちのが速いんじゃね?
けど、文字ごとにメソッドを呼ぶオーバーヘッドもあるし?
というわけで試してみる。 2 回呼んでるならメモ化すればいいじゃないの精神。
def truncate_string2(str, len, omission) widths = Hash.new{|h, k| h[k] = Unicode::DisplayWidth.of(k) } return str.dup unless str.each_char.inject(0){|r, c| r + widths[c] } > len lim = len - widths[omission] n = 0 chars = str.each_char.take_while{|c| (n += widths[c]) <= lim } chars.join << omission end
ベンチ。
user system total real ascii: no memo 2.309000 0.000000 2.309000 ( 2.298512) ascii: memo 0.562000 0.000000 0.562000 ( 0.566717) mb: no memo 0.999000 0.000000 0.999000 ( 1.001561) mb: memo 0.733000 0.000000 0.733000 ( 0.725408)
効いてるっぽい。
さらによく考えると、 str を終わりまで舐めて全長を出すのは無駄である。 len を超えた時点で省略するのは確定するんだから。
というわけでほい。
def truncate_string3(str, len, omission) widths = Hash.new{|h, k| h[k] = Unicode::DisplayWidth.of(k) } n = 0 over_len_p = false str.each_char do |c| if (n += widths[c]) > len over_len_p = true break end end return str.dup unless over_len_p lim = len - widths[omission] n = 0 chars = str.each_char.take_while{|c| (n += widths[c]) <= lim } chars.join << omission end
ベンチ。
user system total real ascii: all len 0.577000 0.000000 0.577000 ( 0.569295) ascii: break when over len 0.374000 0.000000 0.374000 ( 0.370433) mb: all len 0.734000 0.000000 0.734000 ( 0.731004) mb: break when over len 0.421000 0.000000 0.421000 ( 0.416125)
これもまた効いてるかな。
とりあえず自分が使うのに問題はなさそうだから一旦満足。
Go 言語で String#eachや IO#eachみたいなことをしたい
忘れがちなので自分用メモ。
string に対して
package main import ( "bufio" "fmt" "strings" ) func main() { var src string = "foo\nbar\nbaz\n" var reader *strings.Reader = strings.NewReader(src) // strings.Reader は io.Reader を実装しているので… var scanner *bufio.Scanner = bufio.NewScanner(reader) for scanner.Scan() { fmt.Println(scanner.Text()) } }
実行結果
foo bar baz
File に対して
package main import ( "bufio" "fmt" "os" ) func main() { var scanner *bufio.Scanner = bufio.NewScanner(os.Stdin) for scanner.Scan() { fmt.Println(scanner.Text()) } // エラー処理はこう。ループのあとでやる。 if err := scanner.Err(); err != nil { fmt.Fprintln(os.Stderr, "Error: ", err) } }
入力
hoge piyo fuga
実行結果
hoge piyo fuga
まとめ
io.Reader に仕立てあげて、そいつを bufio.Scanner にする。
tap break 教に入信しようと思う
便利。
たった一例しか使った記憶がないけど、それだけでも便利。
first_line = File.open("..."){|f| f.gets.tap{|line| break line.chomp if line } }
tap なしだとださいローカル変数がががが
first_line = File.open("..."){|f| line = f.gets line ? line.chomp : line }
line 、 line って何回言ってんだよ! スマホばっか見てる JK かよ!
( 1 回しか違わないんじゃないかな…)
class Object def tap_instance_eval(&block) tap{ ret = instance_eval(&block) break ret unless equal?(ret) } end end first_line = File.open("..."){|f| f.gets.tap_instance_eval{ self and chomp } }
ついに line が居なくなったぜ、ふっ。
(ここまでやったらやり過ぎなんだな…)
ひさびさの自家製はてダラ
で投稿テスト。
org-hatena-diary.el のテスト
org-mode -> はてな記法エクスポータも Emacs からはてなダイアリーの記事を投稿・管理するのも既にあったので手を
組ませてみる。
…実装の仕方に目を瞑りさすれば、なかなかよさげ?
設定例
(setq org-hatena-diary-username "アカウント名") (setq org-hatena-diary-password "APIキー") (global-set-key (kbd "<f12>") 'org-hatena-diary-list)
ライブラリ
;;; org-hatena-diary.el --- Write diary in org-mode and post it to hatena diary -*- lexical-binding: t; -*- ;; Copyright (C) 2015 arikui ;; Author: <arikui.ruby@gmail.com> ;; Keywords: ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see http://www.gnu.org/licenses/. ;;; Commentary: ;; This elisp hacks two libraries, ;; hatena-diary.el(http://d.hatena.ne.jp/tarao/20130110/1357821338) and ;; ox-hatena.el(http://akisute3.hatenablog.com/entry/2013/08/23/150311), ;; to cooperate. ;;; Code: (require 'hatena-diary) (require 'ox-hatena) (eval-when-compile (require 'cl)) (defun org-hatena-diary::advice-hatena-d-parse-content (f &rest args) (save-window-excursion (org-hatena-export-as-hatena) (apply f args))) (advice-add 'hatena-d-parse-content :around 'org-hatena-diary::advice-hatena-d-parse-content) (defun org-hatena-diary::turn-on-org-mode () (when (string= "* Title" (buffer-string)) ; 記事の新規作成の場合 (erase-buffer) (insert "#+TITLE: #+FILETAGS: ") (goto-char (point-min)) (end-of-line)) (org-mode)) (setq hatena:d:major-mode 'org-hatena-diary::turn-on-org-mode) (defvaralias 'org-hatena-diary-username 'hatena:username) (defvaralias 'org-hatena-diary-password 'hatena:password) (defalias 'org-hatena-diary-list 'hatena:d:list) (provide 'org-hatena-diary) ;;; org-hatena-diary.el ends here
Bison/Flex による再入可能字句/構文解析器
いろいろ試行錯誤があったので結論とかをまとめる。
使う API など
Flex 側
Flex が提供する
- `--header-file' オプション
- 他のソースから yylex_init_extra() とかを使うため
- YY_DECL マクロ
- Bison が yylex() のシグネチャを決めつけてくるので歩み寄る
- `%option reentrant' ディレクティブ
- 基本
- yyget_extra() API
- パーサオブジェクトを Flex 側から触る
電卓プログラムからの抜粋 - calc.lex:
%{
#include "./calc.tab.h"
#define YY_DECL int yylex(YYSTYPE* yylval, void* yyscanner)
#define PARSE_STATE yyget_extra(yyscanner)
int yywrap(yyscan_t s){ return 1; }
%}
%option reentrant
%%
// ...
%%
Bison 側
- YYLEX_PARAM マクロ
- パーサオブジェクトから yyscan_t 構造体を渡す用
- `%pure_parser' ディレクティブ
- 基本
- `%parse-param' ディレクティブ
- yyparse() やら yyerror() やらにパーサオブジェクトを渡すため
電卓プログラムからの抜粋 - calc.y:
%{ #define YYDEBUG 1 #include "./parse_state.h" #define YYLEX_PARAM p->scanner void yyerror(struct parse_state *p, const char *msg){ parser_error(p, msg); } %} %pure_parser %parse-param { struct parse_state *p } /* other directives */ %% /* rules */ %%
ハマったところ
Flex の出力したヘッダは、 Flex ソースでインクルードしてはいけない
なんかよく分かりませんが、内部用っぽいマクロをヘッダの最後で #undef しているので、まともに動かなくなります。
parse_state 構造体の定義のために yyscan_t を定義したヘッダが要るな -> parse_state.h で Flex ヘッダをインクルードする ->間接的に Flex ソースにもインクルードされる -> 動かない
結論を言うと、 yyscan_t は 単なる void* の typedef なので、 void* を使えばよいです。
電卓プログラムからの抜粋 - parse_state.h:
#ifndef _PARSE_STATE_H_ #define _PARSE_STATE_H_ struct parse_state { void *scanner; /* ... 他のメンバ ... */ }; void parser_init(struct parse_state *); void parser_destroy(struct parse_state *); void parse(struct parse_state *); #endif /* _PARSE_STATE_H_ */
↑の関数の実装
#include <stdio.h> #include "./parse_state.h" #include "./lex.yy.h" void parser_init(struct parse_state *p){ yylex_init_extra(p, &(p->scanner)); /* ... 他の初期化 ... */ } void parser_destroy(struct parse_state *p){ yylex_destroy(p->scanner); /* ... 他の後始末 ... */ } void parse(struct parse_state *p){ /* 必要に応じて yyset_in() などで設定 */ if (yyparse(p) != 0) { fprintf(stderr, "error occured\n"); } }
Bison の出力したヘッダは、 Bison ソースでインクルードしなくてもよい
トークンと %union の宣言だけで、しかも Bison の出力する .c にも含まれてる内容なので、インクルードしてもまったくの無駄。