t-hom’s diary

主にVBAネタを扱っているブログです。

VBAで関数型プログラマーの思考法を学ぶ

これは私に関数型言語の素晴らしさを教えてくれた本である。

プログラミングGauche

プログラミングGauche

プログラミングGauche(ゴーシュ)。

一度売ってしまったのだけど、どうしてもその本のコラムLisp脳」の謎に迫る-Schemeプログラマの発想がもう一度読みたくなって先日ついに買い戻してしまった。

そのコラムは手続き型の思考と関数型の思考の違いについて説明したもので、初めて読んだときはあまりに考え方が違うので衝撃を受けた。

これはGaucheという言語の本なので、コラムのコードも当然Gaucheで書かれているのだが、今回はこのコラムの内容をVBAのコードに置き換えて紹介ようと思う。
厳密にいうとGaucheは処理系の名称なので、言語名はSchemeだけど、どういうことか説明しだすとややこしいので割愛。

さて、その前に関数型言語について。

関数型言語とは

関数型言語というのは、ざっくりいうと関数を値として扱うことができる言語である。値であるから、変数に入れたり、ほかの関数の引数にしたり、戻り値にしたりすることができる。

関数というとExcel関数をイメージしてしまうかもしれないけど、ここではひとまずVBAのプロシージャに相当するものと考えてほしい。
つまり、プロシージャを変数に入れたり、他のプロシージャに引き渡したり、あるプロシージャの戻り値としてプロシージャを返したりできるのだ。

ちなみにVBA関数型言語ではないので、こんなことはできない。

コラムの内容

このコラムではコードの題材はとしてFizzBuzzを扱っている。そこでまず、FizzBuzzのルールを説明する。

FizzBuzzのルール

  • 1から100まで順に出力せよ
  • ただし、その数が3で割り切れる場合は数字の代わりにFizzと出力する
  • また、その数が5で割り切れる場合は数字の代わりにBuzzと出力する
  • さらに、その数が3と5と両方で割り切れる場合は数字の代わりにFizzBuzzと出力する

手続き型の思考

手続き型の思考では、与えられた課題をどのように実現するか、つまりHowを考え、その通りにプログラムを作成する。

  1. まずForループで変数 i を1~100まで順番に増やす。(How)
  2. ループの中で、3で割り切れるか、5で割り切れるか、3と5で割り切れるかをチェックし、それぞれ対応する結果を出力する。

図で書くと、このようなフローチャートのイメージになる。
f:id:t-hom:20170905113229p:plain

関数型言語を知らない一般的なプログラマーは、概ねこのように考えるだろう。

関数型の思考

関数型の思考では、与えられた課題をクリアするのに何が必要か、つまりWhatを中心に考える。

  1. とりあえず1から100まで入ったリストを用意する。
  2. 次に数値を与えると条件にしたがってFizz、Buzz、FizzBuzz、あるいは数値そのままを返す関数を用意する。
  3. リストに関数をマッピングさせて、結果リストを得る。
  4. 結果リストを出力する。

VBAで関数型の思考法をコードにしてみる

先ほどの関数型の考え方を図にすると、このようなイメージになる。
f:id:t-hom:20170904223107p:plain

左上の1~100と書かれているものは、1~100が格納されたリストである。今回はVBAプログラマ向けの説明なので、1~100が配列に順番に格納されているイメージで良いかと思う。関数型言語にはこのリストを瞬時に作るIota(イオタ)関数というものがある。

VBAに同等のものは存在しないので、自分で作ってみた。

Function Iota(n)
    Dim ret: ReDim ret(1 To n)
    Dim i
    For i = 1 To n
        ret(i) = i
    Next
    Iota = ret
End Function

次にFizzBuzz関数と書かれているものは、数値をひとつだけ受け取って、結果(Fizz、Buzz、FizzBuzz、元の数値のいずれか)を返す関数だ。

VBAではこのようになる。これは関数型言語でも自分でコーディングする部分だ。

Function FizzBuzz(n)
    Dim ret
    Select Case 0
    Case n Mod 15: ret = "FizzBuzz"
    Case n Mod 5: ret = "Buzz"
    Case n Mod 3: ret = "Fizz"
    Case Else: ret = CStr(n)
    End Select
    FizzBuzz = ret
End Function

さて、1つだけ数値を受け取るFizzBuzz関数に、複数の数値が集合した配列をそのまま渡すことはできない。
ここで登場するのがMapという関数。関数型言語には大抵、Map関数があり、リストと関数を引数として渡すと、リストのそれぞれの要素に関数を適用して、戻り値として新しいリストを返してくれる。

※ちなみにMapは一般的には地図と訳すけど、ここでは「対応付ける」という意味。

VBAにはMap関数がないので作ることになるが、VBA関数型言語ではないため関数を引数として渡すことができない。
そこで関数名で実行できるApplication.Runを利用した擬似Map関数を作ってみた。これで関数名を文字列として渡すことができる。

Function Map(function_name As String, list)
    Dim ret: ReDim ret(LBound(list) To UBound(list))
    Dim i
    For i = LBound(list) To UBound(list)
        ret(i) = Application.Run(function_name, list(i))
    Next
    Map = ret
End Function

これで結果配列を得られるようになったので、あとは出力だけ。
関数型言語にはリストをそのまま出力する関数が備わっているが、VBAには無いので配列をすべて出力するプロシージャを新たに作る。

Sub PrintList(list)
    Dim i
    For i = LBound(list) To UBound(list)
        Debug.Print list(i)
    Next
    Debug.Print
End Sub

そしてすべてをつなぐメインコードはこうなる。

Sub Main()
    Call PrintList(Map("FizzBuzz", Iota(100)))
End Sub

Iotaで作られた配列にFizzBuzz関数がマップされ、その結果をプリントするというコードになっている。

今回はVBAなので、Iota関数、Map関数、PrintListプロシージャでそれぞれループを使用しているが、関数型言語ではこれらの関数はあらかじめ用意されているので実質ループは必要ない。
材料である1~100のリストはIotaでポンと作れるし、そこに自作の関数をMapさせてはい出来上がりという世界だ。関数型言語なら、実際のコーディングはFizzBuzz関数と、メインコードのみで済む。

まとめ

Schemeを学ぶとプログラミングの筋がよくなるといわれている。そこで今回は私が特に感銘を受けたプログラミングGaucheのコラムをVBAコードを使って紹介した。
自分なりの言葉で説明したので受ける印象はまたちょっと違うかもしれないけれど、この記事をきっかけに関数型言語にも興味を持っていただけたらと思う。
手続き型プログラマーにとっては、非常に取っつきにくい言語であるが、だからこそ学びも多い。

ExcelでネストしたIf関数をVBAでインデントして分析しやすくする

ExcelでIF関数を使うと条件によっていろいろと処理を変えることができる。
複雑な条件はIF関数を組み合わせることで実現できるが、やりすぎるとすごく見づらい。

たとえば、以下のような式を作ってみた。

=IF(条件,IF(条件,IF(条件,TRUE,FALSE),IF(条件,IF(条件,FALSE,TRUE),FALSE)),FALSE)

ネストしすぎて解析が困難になっている。

これを解析用にインデントしてみると、このような構造になっていることがわかる。
f:id:t-hom:20170820115954p:plain

VBAと違ってElse文はないけれど、このようにインデントすれば真と偽の対応がわかりやすい。
【注意】解析用のインデントなので、最終的にはタブと改行を削除して元に戻さないと式として使えない。

今回はこの改行とインデントを自動で行うマクロを作ってみた。
技術的にはコンパイラの字句解析を応用したものだ。
これは説明するとすごく長くなるので割愛し、コードだけ貼り付けておく。
※末尾に参考文献を貼り付けておくので興味がある方はどうぞお買い求めください。

コード

準備するモジュールは4つある。

標準モジュール1つ

  • Module1

クラスモジュール3つ

  • Expression
  • Token
  • Stack

Module1のコード

プログラムのエントリーポイント(開始場所)はMainプロシージャである。

Public Enum TokenType
    Target  'If
    BeginParen  '(
    EndParen    ')
    Comma   ',
    Other
End Enum
Public Enum CharType
    Alphabet
    Number
    BeginParen
    EndParen
    Comma
    DoubleQuote
    Other
End Enum

Function GetCharType(c) As CharType
    Dim ret As CharType
    Select Case Asc(c)
    Case Asc("a") To Asc("z"), Asc("A") To Asc("Z")
        ret = CharType.Alphabet
    Case Asc("0") To Asc("9")
        ret = CharType.Number
    Case Else
        Select Case True
            Case c = "(": ret = CharType.BeginParen
            Case c = ")": ret = CharType.EndParen
            Case c = ",": ret = CharType.Comma
            Case c = """": ret = CharType.DoubleQuote
            Case Else
                ret = CharType.Other
        End Select
    End Select
    GetCharType = ret
End Function
Function IsIn(target_, ParamArray check()) As Boolean
    Dim i As Long, ret As Boolean: ret = False
    For i = LBound(check) To UBound(check)
        ret = ret Or check(i) = target_
    Next
    IsIn = ret
End Function

Sub Main()
    Dim targetExpression As Expression: Set targetExpression = New Expression
    targetExpression.ExpressionString = InputBox("数式を入力してください。")
    
    Dim tokens As Collection
    Set tokens = GetTokens(targetExpression)
    Dim t As Token
    
    Dim st As Stack: Set st = New Stack
    Dim tabCount As Long
    For i = 1 To tokens.Count
        Debug.Print tokens(i).tString;
        Select Case tokens(i).tType
            Case TokenType.BeginParen
                If tokens(i - 1).tType = TokenType.Target Then
                    st.Push True
                    tabCount = tabCount + 1
                Else
                    st.Push False
                End If
            Case TokenType.EndParen
                If st.Pop Then tabCount = tabCount - 1
            Case TokenType.Comma
                If st.Top Then
                    Debug.Print
                    Debug.Print String(tabCount, vbTab);
                End If
            Case Else
        End Select
    Next
End Sub

Function GetTokens(targetExpression As Expression) As Collection
    Dim ret As Collection: Set ret = New Collection
    
    Dim t As Token
    Do While targetExpression.hasNext
        Set t = New Token
        t.tString = targetExpression.getNext
        Select Case GetCharType(t.tString)
            Case CharType.Alphabet
                Do While IsIn(GetCharType(targetExpression.checkNext), CharType.Alphabet, CharType.Number)
                    t.AddChar targetExpression.getNext
                Loop
            Case CharType.Number
                Do While GetCharType(targetExpression.checkNext) = CharType.Number
                    t.AddChar targetExpression.getNext
                Loop
            Case CharType.DoubleQuote
                Do While GetCharType(targetExpression.checkNext) <> CharType.DoubleQuote
                    t.AddChar targetExpression.getNext
                Loop
                t.AddChar targetExpression.getNext
            Case CharType.BeginParen
            Case CharType.EndParen
            Case CharType.Comma
            Case CharType.Other
        End Select
        ret.Add t
    Loop
    Set GetTokens = ret
End Function

Expressionクラスのコード

Public ExpressionString
Private cursor
Private Sub Class_Initialize()
    cursor = 1
End Sub

Function hasNext() As Boolean
    hasNext = Len(ExpressionString) > cursor - 1
End Function

Function getNext() As String
    getNext = Mid(ExpressionString, cursor, 1)
    cursor = cursor + 1
End Function

Function checkNext() As String
    If hasNext Then
        checkNext = Mid(ExpressionString, cursor, 1)
    Else
        MsgBox "error"
    End If
End Function

Sub Reset()
    cursor = 1
End Sub

Tokenクラスのコード

Public tString
Sub AddChar(c)
    tString = tString & c
End Sub

Property Get tType() As TokenType
    Dim ret As TokenType
    Select Case UCase(tString)
        Case "IF": ret = TokenType.Target
        Case "(": ret = TokenType.BeginParen
        Case ")": ret = TokenType.EndParen
        Case ",": ret = TokenType.Comma
        Case Else: ret = TokenType.Other
    End Select
    tType = ret
End Property

Stackクラスのコード

Private Items() As Variant

Property Get Count() As Integer
    Count = UBound(Items)
End Property

Property Get Top() As Variant
    Top = Items(UBound(Items))
End Property

Public Function Pop() As Variant
    If UBound(Items) > 0 Then
        Pop = Items(UBound(Items))
        ReDim Preserve Items(UBound(Items) - 1)
    Else
        Pop = Empty
    End If
End Function

Public Sub Push(ByRef x As Variant)
    ReDim Preserve Items(UBound(Items) + 1)
    Items(UBound(Items)) = x
End Sub

Private Sub Class_Initialize()
    ReDim Items(0)
End Sub

実行してみる。

Module1のMainプロシージャを実行すると、インプットボックスが表示されるので適当なネストしたIF関数を入れる。
f:id:t-hom:20170820121126p:plain

OKをクリックすると、イミディエイトウインドウ内にインデントされた式が出てくる。
f:id:t-hom:20170820121303p:plain

ネストしたIFを考えるのが面倒なので、以下のサイトからサンプルをいくつかいただいてきた。
Excel(エクセル)関数の技:IF関数のネスト(入れ子)の方法

例1)

=IF(C3="","",IF(C3>60,"○",IF(C3>30,"△","×"))

=IF(C3="",
    "",
    IF(C3>60,
        "○",
        IF(C3>30,
            "△",
            "×"))

例2)

=IF(AND(C3>30,C3<=60),"△",IF(AND(C3>=0,C3<=30),"×",IF(AND(C3>60,C3<=100),"○","")))

=IF(AND(C3>30,C3<=60),
    "△",
    IF(AND(C3>=0,C3<=30),
        "×",
        IF(AND(C3>60,C3<=100),
            "○",
            "")))

例3)
日本語まじりの抽象式でもこのとおり。

=IF(論理式1, [真の場合1], IF(論理式2, [真の場合2], IF(論理式3, [真の場合3], [偽の場合3])))

=IF(論理式1,
     [真の場合1],
     IF(論理式2,
         [真の場合2],
         IF(論理式3,
             [真の場合3],
             [偽の場合3])))

参考文献

初版48ページ 字句解析プログラム

【注意】VBAの書籍ではありません。C言語で書かれてます。

2017/08/26 修正

GetCharType関数でNumberの判定が1~9になってたのを0~9に修正しました。
※今回のIFの判定に影響はありません。

Prism.jsの紹介と日本語文字列が認識されない件の対応方法

目次

Prism.js(プリズム)とは

Prism.jsとはWebサイトやブログにプログラムのソースコードを掲載する際、要素ごとに色付けしてくれるツールである。

以下は、Prism.jsの適用前と適用後のイメージ。
f:id:t-hom:20170729152610p:plain

私のWebサイトの一部では同種のツール「SyntaxHighlighterと」を利用しているが、Prism.jsの方がサイズが軽量で動作も速いとのことなのでこちらに乗り換えようかと検討中。

※ちなみに、はてなブログはわざわざツールを使わなくでもコードの色分機能が最初から備わっている。今回は別のサイトの話。

さて、今回Prism.jsを試してみたところソースコード中に日本語の文字列が出てきたときにうまく認識されない問題でハマったので、備忘録として残しておく。

その前に使い方から説明しよう。

Prismの入手

※画像は2017年7月29日現在のものです。

以下のURLにアクセスし、
http://prismjs.com/

Downloadボタンでダウンロードページを開く。
f:id:t-hom:20170729155612p:plain


すると、下の方にいろいろと選択できる画面が出てくる。
f:id:t-hom:20170729155932p:plain

まず一番上のラジオボタンから説明
Compression level: ←圧縮レベル
Development version ←開発版
Minified version ←圧縮版なので、こちらでOK

この圧縮というのはJavaScriptのソースから実行に不要なスペース等を除いたもの。
開発版は人間が読みやすいようにスペースでインデントされている。

そのすぐ下にあるCoreはプログラムの中核部分。当然必要なのでチェックは外せなくなっている。

Themesはデザインの違いなので好きなものを選べばよい。

Languagesは自分がWebに掲載するであろうソースコードの言語を選択する。
最初からMarkup、CSS、C-like、JavaScriptにチェックが入っているが、これらのコードを掲載する予定がなければ外して良い。

私はVBAを掲載したいけれど、選択肢が無いのでVB.Netにチェックを付けた。こうするとBASICにも自動でチェックが入るが、これはVB.Netの言語定義がBASICの言語定義に依存しているため。

Pluginsは必要に応じてチェック。コードの色分けだけならプラグインは不要なので、こちらの解説は他サイトに譲る。

あとは一番下までスクロールして、JSとCSSの2つをダウンロードする。
f:id:t-hom:20170729161640p:plain

Prismの使い方

ダウンロードしたcssとjsファイルをheadタグ内で参照させて、コードを書く際はpreタグとcodeタグで囲む。
codeタグのclassに"language-対象の言語名"を指定することでその言語用にハイライトされる。

<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<link rel="stylesheet" href="prism.css" />
	<script src="prism.js"></script>
	<title></title>
</head>
<body>
	<pre>
		<code class="language-vbnet">
Sub Hoge()
    Dim Age As Long: Age = InputBox("年齢を入力し@てください。")
    MsgBox "あなたは" &amp; IIf(Age &gt;= 20, "成年", "未成年") &amp; "です。"
End Sub
		</code>
	</pre>
	
</body>
</html>

日本語文字列の問題

Prism.jsは軽量で使い勝手も良さそうだが、残念なことに現時点(2017/7/29)では日本語に対応していない様子。
文字列中に日本語があると表示が崩れてしまう。
f:id:t-hom:20170729162606p:plain
※今回はvbnetのみで、他の言語では試してないので不明。

Prism.jsを日本語文字列に対応させる方法

そもそもの原因は、Prism.jsのここの部分↓
f:id:t-hom:20170729163044p:plain

「string(文字列)とは」という定義が正規表現で書かれているけれど、これが半角文字しか考慮されていない。
ここを次のように編集する。

まず編集前がこちら。

string:/"(?:""|[!#$%&'()*,\/:;<=>?^_ +\-.A-Z\d])*"/i

編集後がこちら。

string:/"(?:""|[^\"])*"/i

編集前はこれとこれとこれと。。という風に全文字を網羅しようとしてたけれど、「"」以外もしくは「""」を文字とした正規表現に変更した。
よく見ると元の正規表現だとアットマークも漏れてるし、問題は全角だけじゃなかったようだ。

本当はgithubでプルリク(?)とかした方が良いんでしょうけど、今のところgithubは全然素人なのでTwitterで直接@prismjsさんに突撃中。

7/30 追記:コード中のコメントもおかしいのでPrism.jsを再修正

うまくいったと思ったけどまだ問題があった。
コメント中の文字列やキーワードまで色分けされてしまう。

これは評価順がstring→commentの順に並んでいるから最初にマッチした方にヒットしてしまうためだ。

Prism.jsのコードで以下の赤字と青字の部分を入れ替えれば改善される。
Prism.languages.basic={string:/"(?:""|[^\"])*"/i,comment:{pattern:/(?:!|REM\b).+/i,inside:{keyword:/^REM/i}},number:(以降省略

↓修正後
Prism.languages.basic={comment:{pattern:/(?:!|REM\b).+/i,inside:{keyword:/^REM/i}},string:/"(?:""|[^\"])*"/i,number:(以降省略

【注意】ブラウザの表示上改行が入って見えるかもしれませんが、実際には1行です。

Wordでメンテナンス性の高いドキュメントを作る鍵はスタイルと描画キャンバス

前回、業務マニュアルを作る際に重要な3つのポイント(指示の明確さ、メンテナンス性、読みやすさ)について書いた。
thom.hateblo.jp

今回はこのうち、メンテナンス性について、具体的にWordをどう使えばメンテナンス性の良いドキュメントができるのかを紹介する。

文書構造を考える

Wordは直観的に操作できるのでその機能を知らなくてもある程度使えてしまう。
これが最大の落とし穴である。

よくある過ち

過ちというのは言い過ぎかもしれないけれど、メンテナンスが面倒くさくなる例として、以下にサンプルの文書を用意した。
f:id:t-hom:20170629012924p:plain

一見問題無いように見えるかもしれないが、これは見出しや表題に対して個別に書式を設定しており、ルーラーでインデントを整えたものだ。スタイルを使わないので全体が標準スタイルとなっている。

構造としては、以下のようなイメージ。
f:id:t-hom:20170629014532p:plain

まず、手動で書式を設定しているため、同じ見出しのはずが微妙にフォントが違う。
f:id:t-hom:20170629014204p:plain

次に、見出しをインデントで表現しているため、その見出しに合わせて本文のインデントも複数種類生まれてしまう。これは後々面倒くさい。
f:id:t-hom:20170629014423p:plain

最後に画像の扱いが前面に浮かせて配置している点。
これはミスして本文を隠してしまっている。
f:id:t-hom:20170629015039p:plain

微調整のために改行を入れると、オートシェイプ2つがついてこず、手動で微調整するハメに。
f:id:t-hom:20170629015545p:plain

画像やシェイプを前面配置にすると本文行から解放されて自由に配置できるように思えるかもしれないが、実際には特定の行と結びついている。

編集記号をすべて表示させる設定を行うことで、具体的にどの画像がどの行に結びついているのかがわかる。
以下は洗濯機を選んでアンカーを表示させた例。
f:id:t-hom:20170629015804p:plain

では画像のグループ化はどうかというと、これもWordの場合はひとつずつ要素を選択する必要があり、面倒くさい。それにグループ化した後に要素を追加したくなったらそのたびに再グループ化の手間がかかる。

画像の扱いに関する解決策は後程。

シンプルで変更に強い文書構造

文書を定型ブロックの積み重ね構造で考えるとシンプルに作ることができる。
図解するとBefore・Afterはこうなる。
f:id:t-hom:20170629022025p:plain

以降、これを実現するための具体的なポイントを紹介する。

Wordで変更に強い文書を作るポイント

Wordで変更に強い文書を作るポイントは以下の5つ。

  • 個別の書式設定をやめて定義したスタイルだけを用いる
  • 画像の直接配置をやめ、描画キャンバスを用いる
  • 画像と本文は横にならべない
  • インデントによる階層構造表現をあきらめる
  • 見出しの階層は2~3つまでとする
個別の書式設定をやめて定義したスタイルだけを用いる

Word2010以降はスタイルがリボンのホームタブにある。(2007は使ってないので不明)
要は、ここにある「表題」「見出し1」「見出し2」を使うということ。
f:id:t-hom:20170629022827p:plain

標準で用意されたスタイルはダサいかもしれない。
f:id:t-hom:20170629023317p:plain

ただスタイルさえ使っておけば、スタイルセットを変更することで、文章のデザインは後から簡単に切り替えができる。
f:id:t-hom:20170629023624p:plain

気に入ったスタイルセットがなければ一旦近いものを選択し、見出しなどを個別に変更しよう。これも直接文書に対して変更するのではなく、編集したいスタイルを右クリックして変更を選択する。
f:id:t-hom:20170629023809p:plain

スタイルの変更ウインドウが開くので、そこから書式ボタンをクリックして細かい部分を定義することができる。
f:id:t-hom:20170629024019p:plain

この作業は面倒だが、一度スタイルを作ってしまえばテンプレート化していくらでも使い回しができる。

画像の直接配置をやめ、描画キャンバスを用いる

描画キャンバスは、Word2003の頃オートシェイプを挿入しようとして自動で出てきたものだ。そのメリットを知らない方からするとうっとおしい奴に思えただろう。人気がなかったためか、Word2010では自動挿入されなくなった。

しかし描画キャンバスを使えば、中に配置した個々の画像は完全に行から解放され、パワポのように自由に要素をレイアウトできる。

Word2010の場合は、挿入タブの図形の一番下に描画キャンバスがある。
f:id:t-hom:20170629024546p:plain

私の場合は、中身の大きさや数にかかわらず常に描画キャンバスを挿入するようにし、サイズは本文幅、背景色はRGBで250,250,250としている。
背景をうっすら塗ることでキャンバスが見えるので編集しやすい。
f:id:t-hom:20170629025253p:plain

背景色は後のメンテナンス時に役立つので、マニュアルが完成しても取らない。

※濃いとこのように白背景の画像などが目立ってしまう。
f:id:t-hom:20170629025839p:plain

さて、画像ついでに、マニュアルでよく使う赤枠について過去記事を書いたのを思い出したのでご参考までに。
thom.hateblo.jp

画像と本文は横にならべない

画像と本文を横にならべるとは、つまりこういうこと。
f:id:t-hom:20170629032412p:plain

これもよくやるアンチパターンだ。

デメリットは以下の3点。

  • 画像が取れる幅が小さいため、細かい部分が見づらい
  • 本文幅が狭いので一般的に読みやすい文字数とされる1行あたり35~40文字の確保ができない
  • 本文量が変わるたびに画像とのズレを微修正する手間がかかる

メンテナンス性が悪いので、私はこの手法を捨てて、本文→画像→本文と単に交互に表示させることにした。
本文が主で画像は補足なので、説明分に続いて対応する画像を表示させることで統一させる。上下が逆になると混乱を来たすので注意。

インデントによる階層構造表現をあきらめる

デザイン上、本文は見出しより右にあるのが好ましい。
つまりインデントを使うということは、こういうイメージだろう。
f:id:t-hom:20170629030420p:plain

でも経験上、常に大見出し→小見出し→本文の構造を維持できるかというと、これは難しい。
小見出しをつけるまでもなく大見出しの直下に本文が欲しくなったり、逆に中見出しをつけたくなったりする。

すると本文のインデント位置もそれに合わせて個別に調整するハメになる。
f:id:t-hom:20170629030819p:plain

つまりインデントはよほど上手く使わないとスタイルが乱立し、シンプルな文書構造が破たんする。

私はもう、インデントで階層構造を表現することは諦めた。
見出しの階層構造は単に文字サイズで表現し、見出しも本文も左位置は揃える。こうすると本文スタイルが「標準」のみで済むのでメンテナンスが楽だ。

見出しの階層は2~3つまでとする

スタイルを覚えると、見出し3、見出し4…とアウトラインに凝るようになるけれど、これもアンチパターン
ナビゲーションを考えたとき、スタイルによる見出しが欲しいのは主にページをまたがるケースである。したがって、大抵は大見出しと小見出しの2種類があれば事足りる。
ただ実際には部分的にもっと小さな見出しが欲しくなることがある。これはスタイルや書式ではなくて先頭に「■」をつけたり「《》」で囲ったりと、テキストによる目印で見出しの代わりとすると良い。なぜなら細々とスタイルを作っても部分的にしか使われない見出しになるからだ。

こだわりを捨て、制約の中でシンプルなドキュメントを作る

いくつかテクニックを紹介してきたが、いずれも中心となるのはスタイルと描画キャンバスの使いこなしである。
これらを習得する目的はシンプルでメンテナンス性の高いドキュメントを目指すということ。

つまり、これだ。
f:id:t-hom:20170629022025p:plain

  • 文書を定型・定幅のブロックの積み重ね構造で考えること。
  • ブロックの種類を増やしすぎず、シンプルで厳選されたものに絞ること。
  • テンプレートをしっかり設計し、テンプレートから外れることをしない。

そうすると制約が生まれ、表現の幅がせまくなる。
だからこそ、工業製品のように効率的に大量生産できる。

体裁に時間をとられず、コンテンツに100%集中できるドキュメント作成プロセスの確立。
この記事がその一助になれば幸いである。

Wordのスタイルを理解するための書籍

Wordの学習には、西上原 裕明さんのWordで作ったWordの本シリーズがベスト。
以下はその一部。

根本から理解する Wordの「スタイル」活用読本 [Word2010/2007/2003/2002対応] (Wordで作ったWordの本)

根本から理解する Wordの「スタイル」活用読本 [Word2010/2007/2003/2002対応] (Wordで作ったWordの本)

ツボ早わかり Wordのエッセンス速習読本 [Word2010/2007対応] (Wordで作ったWordの本)

ツボ早わかり Wordのエッセンス速習読本 [Word2010/2007対応] (Wordで作ったWordの本)

Wordの「何でこうなるの?」解消事典 ?不審な挙動の処方箋〔Word2010/2007/2003/2002対応〕 (Wordで作ったWordの本)

Wordの「何でこうなるの?」解消事典 ?不審な挙動の処方箋〔Word2010/2007/2003/2002対応〕 (Wordで作ったWordの本)

業務マニュアルを作る際に重要な3つのポイント

今回は業務マニュアルを作る際に重要な3つのポイントを紹介する。

私が重要だと思うポイントは、指示の明確さ、メンテナンス性、読みやすさの3点である。
以下にそれぞれ詳しく解説する。

指示の明確さ

業務マニュアルは作業者に対し、何をすればよいのか明確に伝える必要がある。
これまで見てきた指示文のスタイルはおおむね3パターンある。

行為名で終わるパターン

以下の例では「行為名」で終わっているためコンパクトにまとまるが、状況によって勘違いの元になる。

1) ○○の設定変更
2) ○○をクリック
3) サーバーの再起動

たとえば、手順2で○○をクリックしたことで自動的に再起動されるにも関わらず上記のような表記になっていると、作業者への指示が不明瞭ということになる。

動詞で終わるパターン

以下の例では「動詞」で終わっているためややコンパクトにまとまるが、こちらも状況によって勘違いの元になる。

1) ○○の設定を変更する
2) ○○をクリックする
3) サーバーを再起動する

上の例はシンプルなので間違えようがないと思うが、たとえば登場人物が複数になり、システムによって処理される項目も含まれてくると、作業者への指示と、そうでないものを混同してしまうこともあるかもしれない。

指示で終わるパターン

以下の例では「指示」で終わっているため文が長くなるが、作業者への指示であることが明確だ。

1) ○○の設定を変更してください。
2) ○○をクリックしてください。
3) サーバーを再起動してください。

これが一番確実に伝わると思うので、私が今後作るものは指示終わりで統一しようかと思っている。

メンテナンス性

マニュアルは業務の変化に合わせて常に最新に保たれなければならないが、メンテナンス性が悪いと更新が億劫になる。
特に普段自分ひとりで対応している業務はマニュアルを見なくてもできてしまうため、マニュアルの更新を後回しにしたり、更新するつもりで失念してしまうことだってある。
しかしいざ自分が休んだとき、引き継いだ方が古いマニュアルを参照してしまうとミスオペの元になるのでこれは大変危険。

更新が億劫にならないよう、メンテナンス性の高いマニュアル作りが重要だと思う。

最近気づいたのだが、メンテナンス性を高めるために一番重要なのは、「凝らない」ということだと思う。
基本的にはWordのスタイル機能を活用してテンプレートを作ってしまい、あとはテンプレート任せにしてしまうのが楽だ。

【参考】スタイル機能については以下のページで簡単に紹介した。
Wordの真価 - You.Activate

メンテナンス性についてはWordのテクニックなども関係してくるので後日別途記事を書くつもりである。書いた。
thom.hateblo.jp

読みやすさ

読みやすさにも色々な観点があり、ここでは閲覧性・一覧性・本文の読みやすさについて紹介する。

閲覧性

欲しい情報にすばやく辿りつけること。
見出しを正しく設定することでナビゲーションバーからアクセスできたり、見出しを目だたせたり表現をわかりやすく工夫することで知りたい情報を見つけやすくする。

一覧性

見渡しの良さのこと。手順数に比べてページ数を使いすぎると、延々とページめくりさせられるようなマニュアルになり、一覧性が下がる。
一覧性を下げる大きな要因は、説明のための画像である。不要な箇所をトリミングしたり、全体の一部を拡大するような表現でコンパクトにすると一覧性があがる。

悪例) 縦幅が大きく、一覧性を下げるスクリーンショット
この例では全体を収めようとしているため必要部分が小さくて見づらいというデメリットもある。
f:id:t-hom:20170628032434p:plain

改善例 1) 不要部分をトリミング
トリミングは画像を選択した際に表示される「図ツール」タブから行える。
ついでに少し大きくリサイズして見やすくした。
f:id:t-hom:20170628032605p:plain

改善例 2) 全体を小さく見せつつ、必要部分だけズーム
ズーム画像はクイックスタイルで影をつけて前面に浮かせたような表現にする。
ズームの台形の作り方は、四角形を配置して右クリックし、頂点の編集を行い、枠線を消して塗りつぶしをグラデーションにする。
ズーム方向に色が薄くなるグラデーションにすると色の拡散でズームが表現できる。
f:id:t-hom:20170628032947p:plain

本文の読みやすさ

本文の読みやすさは、フォント、行間、行幅、書式で決まる。

フォントについて

印刷する場合、本文は明朝、見出しはゴシックにするのが基本。また強調したい単語もゴシックにする。
また、日本語の場合は等幅フォント、英語は文字によって幅が変わるプロポーショナルフォントが基本。
※ちなみにMS明朝は等幅、MS P 明朝はプロポーショナル。

ただしMS明朝・MSゴシックはドットの粗が目立つので、主に画面で見る場合はメイリオが読みやすい。メイリオプロポーショナルフォントだが日本語でもそれほど字詰まりを感じない。

f:id:t-hom:20170628035952p:plain

Wordでメイリオを使う際の注意点は以下参照。
thom.hateblo.jp

本文のフォントサイズはユニバーサルデザインを考慮して12ポイントがおススメ。
※職場の年齢層によっては14ポイントくらいあっても良いかもしれない。

行間について

行間が詰まりすぎたり、広すぎたりすると読みづらくなる。文字サイズの0.5~0.8倍くらいが良いと思う。

【参考】
文章の作り方|伝わるデザイン

行幅について

行幅は1行あたりの文字数で決まる。
狭すぎるても広すぎても読みづらいので、35~40文字程度がおススメ。

書式について

赤字・太字・下線を多用するとゴテゴテするのでよろしくない。
thom.hateblo.jp

市販の書籍をイメージしてマニュアルを作る

私は業務マニュアルを作る際、市販の書籍をイメージする。
小説などはあまり参考にならないので、プログラミングの学習書やHow To本など、何かを解説している書籍を参考にすると良い。

市販の書籍はプロの編集者がフィルターになっている為、非常に読みやすい。
強調表現であるゴシック体や太字がどういう箇所にどのくらいの割合で使われているか、一行あたりの文字数はどれくらいか、文字サイズに比べて行間のサイズはどうか、見出しの階層はどれくらい深いかなど、読みやすい文書のスタイルは市販の出版物から学べる。

ただし、何を真似るかは適切に取捨選択すること。
市販の書籍は売るために手間をかけて工夫を凝らすが、業務マニュアルにおいてその手間がメンテナンス性の低下につながるようではいけない。
マニュアル作成は凝りだすとキリがないので、その工夫はオペレーションミスを減らすのか、それともただの自己満足なのかよく自問したい。

書籍紹介

業務システムのためのユーザーマニュアル作成ガイド

業務システムのためのユーザーマニュアル作成ガイド

こちらはマニュアル作成の指針から適切な文章の書き方、マニュアルの校正や保守までマニュアルにかんするあらゆるトピックを扱った貴重な本。ITエンジニア必携とあるが、まさにそのとおり。マニュアル作成に携わるすべての人に読んでほしい書籍だ。

根本から理解する Wordの「スタイル」活用読本 [Word2010/2007/2003/2002対応] (Wordで作ったWordの本)

根本から理解する Wordの「スタイル」活用読本 [Word2010/2007/2003/2002対応] (Wordで作ったWordの本)

こちらはWordでスタイルを使いこなすために必要な知識が得られる本。メンテナンス性の高いドキュメントを作るには、まずスタイルの理解から。知っているのと知らないのでは全く効率が変わってくる。

VBAとマクロの違い ~ マクロの語源はギリシャ語

今回はVBAとマクロがどう違うのか、雑学的なお話。

よくある勘違いが以下の2つ。

  • マクロの記録によって作られたものがマクロだ。
  • マクロとVBAは同じものだ。

どちらも間違っている。

巷で正確な解説を見かけないのはたぶん、厳密に話し出すと長くなるからだろう。あるいは説明を書いている本人もよくわかってないのかもしれない。

別にVBAとマクロの違いについて正確に説明できなかったからといって特段困ることは無いけれど、よくわからずにマクロだのVBAだのと言ってるのもちょっとモヤっとすると思うので、ちょっとここらで真剣に説明しようと思う。

マクロとは何か

マクロ…まずこの言葉が曲者だ。
これはもともとマクロ命令(macro instruction)と呼ばれていたが、いつしか略されるようになったものだ。

【参考】マクロ命令 - 意味・説明・解説 : ASCII.jpデジタル用語辞典

命令であることはわかった。
それで、「マクロ」とは?

実はこれ、もともとはギリシャ語で大きいという意味の言葉(μακρο)。

ウァクポ?いや、これでマクロと読む。
英語のuに見えるのはMに相当するミュー。pに見えるのはRに相当するローである。
f:id:t-hom:20170618172312p:plain

つまり英語のアルファベットに直すとmakro(マクロ)と書かれていることに。
英語に導入されたときkの綴りがcに置き換わって接頭辞「macro-(おおきな~)」になったと思われる。

マクロ経済学Excelマクロのマクロって実は語源は同じだったという話。
ちなみに小さいを意味するミクロもギリシャ語由来だ。

マクロ命令とは何か

前述の話から、マクロ命令とは「大きな命令」という意味になる。
ただ大きな命令と言われてもピンと来ないと思うので、全自動洗濯機に例えて説明する。

f:id:t-hom:20170618173200p:plain

洗濯機は選択物を入れてスタートボタンを押すだけで全部やってくれるが、内部では注水したりドラムを回したりと小さな命令が飛び交っている。これらをひとまとめにして「洗う」という大きな命令にしたのがマクロ命令。

Excelなら、値を入れて、色を塗って、別のセルを選択してという風にいくつかの小さな命令を組み合わせて作る大きな命令がマクロ命令である。

ただ現在は「マクロ」が「マクロ命令」の略だという話はほとんど知られていないので。マクロ命令と言っても通じないだろう。

「大きい」だけ残して肝心の「命令」を略すの?って思うけど、それ言ったら「ケータイ」もそうだ。「持ち歩き」だけ残して肝心の「電話」が消えてる。

VBAとは何か

VBAはマクロ記述するときに使う言語のことだ。

たとえばこんな風に日本語で書いても動かない。

 A1セルを黄色に塗ってください。

だからこういう風にVBA言語で書く。

 Range("A1").Interior.Color = vbYellow

Microsoft Officeのマクロ機能で使用できる言語はVBAだけなので、VBA=マクロと勘違いされやすいけれど、マクロは複数の命令を組み合わせた大きな命令のことで、VBAはそれを書くための手段なので、視点が違う。

Open Officeマクロなんかは標準のOpenOffice Basic言語のほかに、Python、BeanShell、JavaScriptなどのマクロ記述言語を選択できるのでマクロと言語の違いを説明するときに引き合いに出すと分かりやすいかもしれない。
f:id:t-hom:20170618175331p:plain

複数プロシージャで構成されたマクロ

たとえば以下のようにプロシージャからプロシージャを呼ぶケースがある。

Sub ごあいさつ()
    Call コンチハ
    Call サイナラ
End Sub

Sub コンチハ()
    MsgBox "Hello"
End Sub

Sub サイナラ()
    MsgBox "GoodBye"
End Sub

このとき、「ごあいさつ」プロシージャは、コンチハ・サイナラの2つのプロシージャを呼び出しているので、これらをひっくるめて一つのマクロであるといえる。
一方で、コンチハ・サイナラはそれぞれ単体で実行できるので、それぞれが独立したマクロであるとも言える。
※MsgBox一つしか命令がないのでマクロ命令の説明と矛盾するけど、慣例的にこれもマクロと呼ぶ。

このように複数命令が存在するときは、「ごあいさつマクロ」と「ごあいさつプロシージャ」では指す範囲が変わってくるので注意。

ひとつのマクロはひとつ、あるいはそれ以上のプロシージャから構成される。
マクロは目的を持った完成品、プロシージャは部品、VBAは記述言語といったイメージ。

Excel VBAExcel マクロ

Excel VBAという呼び方があるけれど、実際にExcel専用のVBA言語があるわけではない。VBA言語自体は他のOfficeアプリでも共通のものである。
f:id:t-hom:20170618180818p:plain

VBA言語を用いてExcelを操作する行為やそのノウハウなどを指して便宜的にExcel VBAと呼んでいるだけ。言語名はあくまでVBA

一方、完成したものをExcel マクロと呼ぶのは自然である。

まとめ

最後に冒頭の勘違いに対して訂正をいれてまとめにしよう。

「マクロの記録によって作られたものがマクロだ。」
→No!

マクロの記録はマクロを作る手段の一つであり、実際にはVBAコードが記録される。
そしてVBAを手で書いても、マクロの記録で作成しても、完成したものはマクロと呼ぶ。

「マクロとVBAは同じものだ。」
→No!

マクロは完成した「大きな命令」を指し、それを記述する言語がVBAである。
同じコードに対し、「これはVBAである。これはマクロでもある。」という説明が成り立つことがあるが、2つの用語は切り口が違うので間違えて知ったかぶらないようにしたい。

Javaでテキストファイルから特定文字列を含む行を除いてファイル出力

今回はJavaでテキストファイルから特定文字列を含む行を除外してファイル出力するコードを作ったので紹介。
コマンドライン引数で複数のキーワードを指定して除外できるようにした。

作成の経緯

このブログのアクセスログを分析する際、いつもダウンロードしたcsvをそのままExcelで開いているが、ひと月あたり数万件のデータがあるので私の非力なマシンではちょっと厳しくなってきた。
特に、検索エンジンからの流入を除いた被リンクからのアクセスだけを分析したい場合、google、yahoo、bing、searchといったキーワードをあらかじめ除外しておくとデータ件数が三分の一くらいまで減るのでExcelで扱うのが楽になる。

sedawkでやれば早いけど、インストールしてないうえ、使い方を忘れたので面倒くさい。そこは日曜プログラマーなので、たまにはJavaのリハビリがてら自分で作ってみることに。(もっと面倒くさいだろうというツッコミはナシで)

できたコード

import java.io.*;

class Filter
{
    public static void main(String[] args)
    {
        if(args.length < 1){
            System.out.println("使用例:java Filter [fileName] [Exclude Text1] [Exclude Text2] ...");
            System.exit(1);
        }
        try {
            BufferedReader br =
                new BufferedReader
                    (new FileReader(args[0]));

            PrintWriter pw = new PrintWriter
                (new BufferedWriter(new FileWriter("out.txt")));

            String str;
            boolean flag;
            while((str = br.readLine()) != null){
                flag = true;
                for(int i=1;i<args.length;i++){
                    flag = flag && str.indexOf(args[i]) == -1;
                }

                if(flag) {
                    pw.println(str);
                }
            }
            br.close();
            pw.close();
        }
        catch(IOException e){
            System.out.println("Error:ファイルの読み書きに失敗しました。");
        }
    }
}

コンパイル

javac Filter.java

Eclipseを使うほどのコードでもないので。。

使い方

java Filter [入力ファイル名] [除外テキスト1] [除外テキスト2] ...

例) java Filter 101-2017-05.csv google yahoo bing search

このように除外テキストはいくつでも指定できる。

所感

例外のケアが雑だけど、まぁ自分ひとりで特定の目的に限った利用なのでこれで十分。
しばらくVBAばかりやってたので、等価比較を「=」で書いてしまう癖が出てハマった。
Javaの等価比較は「==」

工夫した点としては複数キーワードの除外判定ループ内で「flag = flag && 条件」としているところ。
これは普段VBAでもよく使ってるパターンで、複数条件を束ねるのに便利。
論理積の性質で、ループ内で1度でもfalseが出ると以降flagはtrueが代入される次の周回までfalseに固定される。

地味に基本情報などのコンピューターサイエンス系の知識が活きるので、にわかプログラマーとの差別化ができてうれしい。

使ってみた感想としては、やはりJavaは動作の速さが印象的。一瞬で処理が終わるので気持ちいい。※初回実行はJava VM起動があるのでほんの少し待たされるけれど。
VBAが遅いんじゃない。あなたのコードが遅いんだ。」なんて巷で良く言われてるけれど、あれは半分本当で半分嘘。正しくは「あなたのコードも遅いけど、VBAそもそも遅い。」だ。
ただしデータ量が少ない場合はVBAでも十分速いのでたかだか数万件ならVBAでも問題なかったかもしれない。

ちなみにVBAで書くと

こうなる。

Sub FilterText()
    '※サンプルなのでダイアログ等使わずに定数で代用
    Const BASE_PATH = "C:\Users\thom\java\", _
        INPUT_FILE_NAME = "101-2017-05.csv", _
        OUTPUT_FILE_NAME = "out2.txt", _
        EXCLUDE_WORDS = "google yahoo bing search"
        
    Dim reader As TextStream
    Dim writer As TextStream
    
    With New FileSystemObject
        Set reader = .OpenTextFile(BASE_PATH & INPUT_FILE_NAME, ForReading)
        Set writer = .OpenTextFile(BASE_PATH & OUTPUT_FILE_NAME, ForWriting, True)
    End With
    Dim str As String, flag As Boolean, ex As String
    Do Until reader.AtEndOfLine
        str = reader.ReadLine
        flag = True
        For Each ex In Split(EXCLUDE_WORDS)
            flag = flag And InStr(str, ex) = 0
        Next
        If flag Then writer.WriteLine str
    Loop
    reader.Close
    writer.Close
End Sub

私のマシンだと約7万行に対して5秒くらい。Java版は一瞬で完了したので、数十万件になってくるとだいぶ差が出てきそう。

参考書籍

こちらは私が初めてJavaを学んだ書籍の第六版。

今回はリファレンスがわりに使ったけど、基本的にLesson1から順に進めていくタイプの本なので、別途リファレンスが欲しくなった。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。