くまくまの業務日誌

Markdown記法で徒然に書いてみましょう。

ログからキーワードを抽出して、ソートして、同じものを切り捨てる 参

圧倒的な高速処理でPythonが決着を付けてしまいました。

早速PythonPowerShellとほぼ同じロジックを組んでみました。出力結果はどちらも同じになります。それはWinMergeを使って確認しました。スクリプトもほぼ同じにしました。

しかし、Pythonの方が圧倒的に早い!時間を計るロジックを入れる気も起きません。
と、同時に感じたのは、どうもPowerShellは、画面出力が異常に遅い。

なので、途中経過を出力させるのをやめて、プログレスバーの出力で途中経過を出力するようにしました。そうすると、もうPowerShellダメだ…と思うほどの処理速度差になってしまいました。 しかも、Pythonで使っているプログレスバーの出力に使ったtqdmは、本当にここに仕込んでいいのかと悩んでしまうところに実装しています。実装に3分程度でした。対して、PowerShellのWrite-Progressは、使い方分かっていたので、偉く実装に手間取りました。こんなところもPythonおそるべし(己の不手際は置いといて…)。

ただ、Python正規表現を扱う際、事前のコンパイルが書きやすかったのでさくっと使っていますが、PowerShellはまだ使い方が分からなかったので使っていません。しかし、そこだけの差ではないと思えています。それと置換後の文字列出力の際、各項目を変数に入れてから使っているところが冗長でした。しかし、これはグループ変数の取り出しが、'\1'とかで取り出せない仕組みの方が悪いような。

さっき調べたら、以下の処理でループ外には出せそうです。でもきっと焼石に水ではなかろうか。

$expression = [regex]"(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}),(\d{3}) (\d{3,5}) (\w+)(\s+-\s+)"
$expression.Replace($row, { 出力用のLambda式 })

もし、読まれている方でロジックやコードに「ここはどうなの?」という方の御意見お待ちしております。

すっかり、読み込むログの形を提示しておりませんでしたが、以下のような感じです。
たしか、カテゴリ部分以降が、%6p - %m%nではなかったかな。

2020-07-10 01:57:28,577 31128 INFO   - ここになんらかのlog4cppで出力したメッセージ

Pythonスクリプト

import re
from tqdm import tqdm

targetLog = r"..\LogParser\hogehoge.log"
outputLog = r".\output.txt"

contents = list()

fout = open(outputLog, "w", encoding="shift_jis")

with open(targetLog, "r", encoding="shift_jis") as f:
    contents = f.readlines()

outputString = "{0}\t{1}\t{2}\t{3}\t{4}\t{5}\n".format(
    "Date", "Time", "ms", "PID", "Category", "Message")
fout.writelines(outputString)

expression = re.compile(r"(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}),(\d{3}) (\d{3,5}) (\w+)(\s+-\s+)")

for row in tqdm(contents):
    if (len(row) == 0):
        continue
    elif (row[0:4] != "2020"):
        continue

    row = row.replace("\t", " ")

    row = expression.sub(r"\1\t\2\t\3\t\4\t\5\t", row)

    #print(row)
    fout.writelines(row)

fout.close()

PowerShellスクリプト

$targetLog = ".\hogehoge.log"
$outputLog = ".\hogehoge.txt"

$contents = Get-Content -Path $targetLog -Encoding OEM

$outputString = "{0}`t{1}`t{2}`t{3}`t{4}`t{5}" -F "Date", "Time", "ms", "PID", "Category", "Message"
$outputString | Out-File $outputLog -Encoding OEM

$count = 0
foreach ($row in $contents) {
    $count += 1

    if ($row.Length -eq 0) {
        continue
    }
    elseif ($row.Substring(0, 4) -ne "2020") {
        continue
    }

    $row = $row.Replace("`t", " ")

    $row = [regex]::Replace($row, "(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}),(\d{3}) (\d{3,5}) (\w+)(\s+-\s+)",
        {
            $date = $args.groups[1].value
            $time = $args.groups[2].value
            $millisec = $args.groups[3].value
            $tid = $args.groups[4].value
            $category = $args.groups[5].value
            $statement = $args.groups[7].value

            "{0}`t{1}`t{2}`t{3}`t{4}`t{5}" -F $date, $time, $millisec, $tid.PadLeft(5, " "), $category, $statement
        })
    
    $progress = "{0}" -F [Math]::round(($count / $contents.Length * 100), [MidpointRounding]::AwayFromZero)
    $running = "processed:{0:#,#}/{1:#,#}" -F $count, $contents.Length
    Write-Progress -Activity "処理中..." -CurrentOperation ($progress + "%") -Status $running -PercentComplete $progress
    
    #Write-Output $row
    $row | Out-File -FilePath $outputLog -Append -Encoding OEM
}

ログからキーワードを抽出して、ソートして、同じものを切り捨てる 弐

そもそもの問題:桁位置がガタガタ

現在使用してるのは、log4cppでして、設定自体はどこにでもあるようなものでした。

設定ファイルに関しては、本当に「詳しくはWebでっ」というやつでして、他の方が詳しく説明してくれているのでググってみてください。

ちなみにですが、最近以下の問題(こちらの使い方の問題)に出くわしてしまって往生しました。

  • config設定中にinfo()で出力しようとすると、VCランタイムDLLエラーに飛んで、プロセスもろとも撃沈。
  • カテゴリあり出力とカテゴリなし出力がどちらも同じファイルに出力(カテゴリありはコンソールにも出力するようにしていた)する設定の場合、スレッドによる同時呼び出しを行うと、これまたVCランタイムDLLエラーに飛んで、プロセスもろとも撃沈。

まず、「config設定中にinfo()で出力」のくだりがおかしいですが、IISのリクエスト処理中に別のリクエストが到着してしまった場合、を想像してください。作りが悪いと、この現象にぶち当たります。これはロジックで対処しましたが、「C++IISにリクエスト送るのって結構大変なのね。」がわかりました。いつか汎用的なコードとして自分に取り込みたいです。

次の、カテゴリあり・なし同時出力ですが、log4cppではカテゴリを作って出力方法を切り替える方法があります。本来であれば「Appender」ですので、Rootにファイル出力、カテゴリを追加して、そこでコンソール出力(親にも伝わるのでそこでファイル出力)とすればよかったのに、Addivirityをfalseにして、Rootとは別物として扱っていたのに、出力ファイルは同じものだったのでバッティング(たぶん排他エラー)で落ちてしまいました。こちらは設定ファイルの修正で対応です。ふぅっ。

しかし、%t(スレッド名)に関しては使っているサンプル自体に出会わないです。一番詳しい説明は、以下の表現ではないかと。

%t ログ要求をしているスレッドの識別子 出典元

私のところの問題は、この%tがスレッドIDであれ、スレッド識別子であれ、3~5桁であることを考慮せず、「%t」で作ってしまったことにあります。こんな些細な修正も簡単に進められないのは、業界の常かと(思いたい)。

しかし、同じメッセージが並んでしたとしても、出力しているスレッドが違うことが、ログからわかることで何度も救われていますので、ここはスクリプトで対応するしかありません。

昔は、桁位置の違いを探りながら進めていました。

そのロジックを闇に葬る前に、反省と成長の証としてここに展開します。

%tがスレッド識別子として3~5桁の可変であるための調整作業。次がカテゴリの長さも、{ERROR, WARN, INFO}で可変だったのでこれまた調整作業。このような調整も、一度プログラムを書いて安定稼働してしまえばいじりなおすこともないだろうと思っていました。

    # Fixed Column
    $date = $row.Substring(0, 10)
    $time = $row.Substring(11, 8)
    $millisec = $row.Substring(20, 3)

    $currentPos = 24

    $processID = ""
    if ($row[$currentPos + 3] -eq " ") {
        $processID = $row.Substring($currentPos, 3)
        $currentPos += 3
    } elseif ($row[$currentPos + 4] -eq " ") {
        $processID = $row.Substring($currentPos, 4)
        $currentPos += 4
    } else {
        $processID = $row.Substring($currentPos, 5)
        $currentPos += 5
    }

    $currentPos += 1

    $category = ""
    $nextOffset = 0
    if ($row[$currentPos + 3] -eq " ") {
        $nextOffset = 3
        $category = $row.Substring($currentPos, $nextOffset)
        $currentPos += $nextOffset
        $nextOffset = 6
    } elseif ($row[$currentPos + 4] -eq " ") {
        $nextOffset = 4
        $category = $row.Substring($currentPos, $nextOffset)
        $currentPos += $nextOffset
        $nextOffset = 5
    } else {
        $nextOffset = 5
        $category = $row.Substring($currentPos, $nextOffset)
        $currentPos += $nextOffset
        $nextOffset = 4
    }

    $currentPos += $nextOffset
    $statement = $row.Substring($currentPos)

ふとしたことで、この鉄板コードと思っていたところにメスを入れることに。

Pythonの試験勉強を行っている際、正規表現の置換に関しての部分を勉強していた際、ふとここのロジックに手を入れるべきと思いつきました。

正規表現では、カッコ()でくくられた部分はグループという概念で、再利用ができます。再利用時は本来ならばカッコの登場順に\1, \2 ... となるのですが、PowerShellとしては、以下のように使います。Groupsでもなく、Valueでもないところにあれっ、と思うところはあります。しかも、groupsは1オリジン?そして、メッセージ部分までカッコでグループ化していませんが、なぜか7番目に入っていたので、そのまま使っています。

途中で{}が登場していますが、どうやらラムダ式として入れているようです。出典元

$row = [regex]::Replace($row, "(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}),(\d{3}) (\d{3,5}) (\w+)(\s+-\s+)",
    {
        $date = $args.groups[1].value
        $time = $args.groups[2].value
        $millisec = $args.groups[3].value
        $tid = $args.groups[4].value
        $category = $args.groups[5].value
        $statement = $args.groups[7].value
        "{0}`t{1}`t{2}`t{3}`t{4}`t{5}" -F $date, $time, $millisec, $tid.PadLeft(5, " "), $category, $statement
    })

正規表現万歳!
これで、スレッド識別子やカテゴリ名でガタガタしてしまっていたログに対して、一発できれいなTSV出力ができます。

最終的に使用しているロジックは以下のようになりました。今はこれの出力結果をExcelに読み込ませて、フィルター機能で問題を探したり、網掛けや文字色変更などで問題点の説明などに使っています。あまりでかいログファイルだとExcelが先に悲鳴を上げますが ^^;

$targetLog = ".\hogehoge.log"
$outputLog = ".\hogehoge.txt"

$contents = Get-Content -Path $targetLog -Encoding OEM

$outputString = "{0}`t{1}`t{2}`t{3}`t{4}`t{5}" -F "Date", "Time", "ms", "PID", "Category", "Message"
$outputString | Out-File $outputLog -Encoding OEM

foreach ($row in $contents) {
    if ($row.Length -eq 0) {
        continue
    }
    elseif ($row.Substring(0, 4) -ne "2020") {
        continue
    }

    $row = $row.Replace("`t", " ")

    $row = [regex]::Replace($row, "(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}),(\d{3}) (\d{3,5}) (\w+)(\s+-\s+)",
        {
            $date = $args.groups[1].value
            $time = $args.groups[2].value
            $millisec = $args.groups[3].value
            $tid = $args.groups[4].value
            $category = $args.groups[5].value
            $statement = $args.groups[7].value

            "{0}`t{1}`t{2}`t{3}`t{4}`t{5}" -F $date, $time, $millisec, $tid.PadLeft(5, " "), $category, $statement
        })


    Write-Output $row
    $row | Out-File -FilePath $outputLog -Append -Encoding OEM
}

しかし、PowerShellの限界なのか、Windowsが悪いのか、この解析ロジックは出力に時間が掛かります。( ´ー`)y-~~
ま、休憩のきっかけになるからいいのですが、ここでPythonとの一本勝負と行きたいところです。

ログからキーワードを抽出して、ソートして、同じものを切り捨てる 壱

コマンドプロンプトでは、よくやるコマンドのテンプレート

ログファイルに対してよくやることは、たぶん以下の作業

  • 履歴をファイルをまとめて1つに集約
  • ファイルの中身を出力
  • ファイルの中身からキーワードを含む行を抽出
  • 抽出した内容をソート
    ログの場合、タイムスタンプがあるし、そもそも昇順
    しかし、タイムスタンプ部をスキップして、メッセージだけをソートしたいことはある
  • ソートした中身から重複するものを切り捨てる
    Windowsには該当コマンドなし
    Unix系にはuniqコマンドがある(要ソート)

いい加減、MS-DOSから脱却したいのにいつまでも、この操作をMS-DOSでやっているので、まじめにPowerShellスクリプトを考えようと思う。

複数のファイルを連結して、1つのファイルに集約

コマンドプロンプトの場合

最初の元凶がここ。スマートな方法があるはず。今まで10個の履歴ファイルを一生懸命書いて連結していたけど、もう飽きた。

copy [ファイル1] + [ファイル2] + [ファイル3] [集約するファイル名]

シェル系

gitをインストールすると、WindowsでもUnixコマンドが使えるので便利だし、任意のフォルダからエクスプローラの右クリックでBash起こせるから、こっちを覚えるべきかもしれないけど、それはまたの機会に。

# 連結するファイル名にワイルドカードを使うと一気に連結
# 順番はきっとASCII順 履歴番号ありとなしで順番が意図しない形になるかもしれない
# Output.log.*
# 順番が重要であるなら、列挙して最後にリダイレクトする。
# 最後にリダイレクトでファイル出力するのを忘れずに

cat [連結するファイル名] > [集約するファイル名]
cat [ファイル1] [ファイル2] [ファイル3] > [集約するファイル名]

PowerShell

まずはネットで、やり方を調べてみると以下の3つが上がってきた。*-Contentで出力するのが正統派に思えてきたけど、リダイレクトもパイプラインとしては王道なので、テンプレート検討としては悩むところ。

Get-Contentで渡すワイルドカードでは、"*.log.*"を指定した場合、"hogehoge.log"は拾ってくれなかったのは困りもの。

Get-ChildItem -File -Filter ["ワイルドカード"] | Get-Content | Add-Content [集約するファイル名]
Get-Content [ワイルドカードで指定するファイル名] > [集約するファイル名]
Get-Content [ファイル名1], [ファイル名2], [ファイル名3] | Set-Content [集約するファイル名]

ちなみに私の職場のログファイルは…
hogehoge.log
hogehoge.log.01
hogehoge.log.02
hogehoge.log.03
となっていて、ちと扱いが辛い。逆順にする必要もある。しかし、これは以下のやり方で解決できた。
これで、ファイル名を逆順に出力させて1つのファイルに集約できる。後はリダイレクトか、*-Contentで出力する。

Get-ChildItem -File -Filter "*.log.*" | Sort-Object -Descending
PS C:\Users\USER\Desktop\work> Get-ChildItem -file -filter "*.log.*" | Sort-Object -Descending


    ディレクトリ: C:\Users\USER\Desktop\work


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       2020/09/12     16:38             16 output.log.03
-a----       2020/09/12     16:37             16 output.log.02
-a----       2020/09/12     16:37             16 output.log.01
-a----       2020/09/12     16:37             16 output.log

コマンドプロンプトで最初に叩く、ログの集約作業はこれで行こう。

Get-ChildItem -File -Filter "*.log.*" | Sort-Object -Descending | Get-Content > [出力ファイル名]

必要なキーワードで抽出する。

MS-DOS

find.exeは昔からの人がつい、手なりで叩いてしまいますが、今は正規表現も使えるfindstr.exeが本流なのでしょう。正規表現のリファレンスはここ(@IT)でも良いかと。

type [入力ファイル名] | find "キーワード名"
type [入力ファイル名] | findstr "キーワード名"

find "キーワード名" < [入力ファイル名]
findstr "キーワード名" < [入力ファイル名]

シェル系

grepは検索の王道なので説明するまでもなく…

cat [入力ファイル名] | grep "キーワード名"
grep "キーワード名" < [入力ファイル名]

PowerShell

正規表現も利用したいですし、Select-Stringを使いましょう。

Get-Content -Path <入力ファイル名> | Select-String -Pattern '正規表現'

ここで、単に ERROR の文字を抽出するのであれば、

Get-Content [入力ファイル名] | Where-Object { $_ -like "*ERROR*" }
Get-Content [入力ファイル名] | Where-Object { $_ -match "ERROR" }

も、ありかもしれません。しかし、シンプルなコードにするなら、Select-Stringがよいと思われます。

データのソートに関しては、ログに関しては桁位置がガタガタ(スレッド名の設定を%tにしてしまったので3~5桁でそろわない)なので、まずここをやっつけなければなりませんが、このネタだけで長くなりそうです。

ということで、次回の講釈で。

Python3 エンジニア認定基礎試験に合格しました。

会社の方針に従う形となりましたが、Python3 エンジニア認定基礎試験を受験し、合格となりました。後から続く人への一助となればと思い、新タグとなるこのPython3認定試験について感想を述べたいと思います。

どうやったら、合格するか。

今回の作戦は、「0円投資で合格」を目標にしました。なので、本は一切買っていません。本来でしたらオライリー・ジャパンの「Pythonチュートリアル 第3版」を購入してからスタートとなるのでしょうが、試験の情報を検索すると、Pythonチュートリアルの章立てと、試験の章立てはほぼ、同じことが分かります。つまり、オンラインの情報だけで受験は可能です。

しかし、オンラインの文章を読むだけでは頭には入らないと思います。C言語をやりながらPythonを勉強すると、「catch、やったっけ...」となりがちです。やはり手を動かして実際にコードを打ち込んで確認する必要は感じました。

幸いなことに、試験範囲にはPythonのインストールに関する設問はありません。既にPythonがインストールされていることを前提とした設問になっています。そこで既に環境が整っている、Google Colabratoryが重宝しました。その存在は知っていたのですが、この試験の動作確認と写経を行うには一番だと思いました。

チュートリアルをひたすら写経

今回は、教本を購入しませんでしたのでなんらかの形で脳に叩き込む必要があります。Colaboratoryでも「コード」と「テキスト」を章立てて入力する事ができます。でもインタープリタの部分は、さすがにシミュレーションとなります。しかし、ほとんどのコードは、コードブロックで入力しながらその動作を確認する事ができます。

他の言語にはない機能を理解する。

既に他の言語を習得していると、Pythonではこうなるのか、という部分が多々発生します。ま、それがPythonのいいところでもあります。

他の機能とは違う部分を理解する。

他の言語では、こう振舞うけどPythonではこう振舞うという部分は試験でミスを誘発しやすいので十分打ち込んで練習しましょう。

  • 例外が発生すると、catchで受けるのではなく、exceptで受けます。
  • forやtryにもelseが書けます。
  • 関数の引数パターンはかなり豊富で、独特です。
  • importで使う場合と、from ~ import で使うパターンは、「モジュール」の言葉の指す所を理解する必要がありますが、標準ライブラリを一通り打ち込んで、使用感を把握しておく必要があります(試験範囲です)。

試験の範囲外にはなるけど、Python-izmは同時に学習しておきたいです。

応用編は、試験の範囲外になるのですが、Pythonチュートリアルを学ぶ際により深い知識を手に入れられます。また、チュートリアルの知識が身についているかの確認の観点で見るのもよいと思います。

模擬試験を活用する。

模擬試験サイトは現時点で以下の2つがあります。

PRIME STUDYは3つの試験が準備されています。 DIVE INTO EXAMは私は2つ受けました。でもどこまでもありそうな雰囲気です。

この2つのサイトを試験直前に実施してみましたが、以下の傾向を感じました。

  • PRIME STUDYの方が習熟度が高くないと解けないかもしれない。
  • DIVE INTO EXAMのレベルの方が、実試験に近いかもしれない。

実際、PRIME STUDYでは75~77.5点しか取れませんでした。なかなか誤解しそうなところを突いてくる試験問題になっている感じです。

DIVE INTO EXAMの模擬試験を受けたら、825、850の得点になりました。実はもっと模擬試験をやった方がいいのかなと思ったのですが、せっかく850点取れたので、自分に「大丈夫」と言い聞かせて早く寝ることにしました。

だんだん得点が高くなる、実際の試験内容に近くなるという形を考えると、PRIME STUDY→DIVE INTO EXAMの順で模擬試験を受けることをお薦めします。

受験の準備、試験会場までの道程の確認

きっと、ここが大事ではないかと。コロナの影響で、自分の近所だが、人がめっちゃ多い VS 人が少なさそうだけど、移動距離がむっちゃある、の選択を余儀なくされました。安直に人が少ない方を選びましたけど、移動時間が長ければ、公共交通機関の利用時間も長くなる。長くなれば、途中の乗り継ぎがうまくいくか、複数のサイトで確認するなどの作業が発生します。現地に1時間前につく余裕も欲しい所です。

OddysseyIDの取得も受験前に必要です。受験票は会場次第との事でしたので、これも確認が必要です。私の場合は5営業日を切っての予約でしたので、念押しの電話も必要となりました。

最後のボタンを押す前に

サマーウォーズ」を思い出しながら、「おねがいしまぁ~すっ」と押すと、きっと合格証が隣の部屋のプリンタから出力されていると思います。 ちなみに、私の会場では「印刷ボタンは押さないでください。」と書かれていた受験会場のマニュアルを渡されながら会場の人から、「最後にこれ押してください(^^;)」と言われて、直前まで惑わされることになりましたが受験勉強の時間はぶれないので、合格と相成りました。

皆様のお役に立てれば幸いです。時間があれば、もう少しここの追記と、Google Colaboratoryについて書いてみたいと思います。

C/C++コーディング規約(草案)

コーディング規約

■前書き

 昨今のプログラム開発における記載ルールは、言語特性やIDEのタイピング補完機能などにより、統一した記載ルール(コーディング規約)を明確にすることで、高速で品質の高いプログラム作成が可能となる。OSや言語の違いにより、使われるルールが変わってくるため、どの場合にはどのようなルールで記載するのかを明確にし、統一された品質の高いプログラム作成の一助となるよう、細部にまでルールを検討したものである。

■基準となる命名規約

 現在、プログラム開発における命名規約は、「ハンガリアン」「パスカル」「キャメル」「スネーク」の4種類が存在している。 それぞれの特徴を把握し、ファイル名、クラス名や構造体名、関数名、変数名の命名ルールを明確にして、統一した開発標準策定を目標とする。

■■ハンガリアン記法

 ハンガリアン記法は、WindowsC/C++開発における標準の命名規約である。WIN32APIは、ほぼこの「ハンガリアン記法」によって、作成されている。型名を変数名先頭に修飾するため、変数の型をコーディング中に推測しやすくなるが、プログラムの改造中に変数の型を変更することになった場合、IDEの機能で安全に変更できるとしても、使用箇所だけ変数名の変更が発生するため、リファクタリングには不向きである。また、Windows開発では、C/C++でのプリミティブな変数をあまり使用せず、#defineやtypedefで置き換えた、大文字の変数型を使用することが多い。

 このハンガリアン記法の概要は以下のようになる。

  1. グローバル変数の場合、変数名先頭に'g_'を付ける。
  2. クラスのインスタンス変数の場合、変数名先頭に'm_'を付ける。
  3. 変数の型がポインタの場合、変数名先頭にポインタを表す'p'または'lp'を付ける。
  4. 変数名先頭にその変数の型を表す修飾子をつける。
実際の型宣言 修飾
char CHAR 'c' cKeyHit
unsigned char BYTE 'by' byBuffer
short SHORT 's' sLength
unsigned short WORD 'w' wSize
int INT 'i' iCounter
unsigned int DWORD 'dw' dwSize
long LONG 'l' lCounter
unsigned long ULONG 'ul' ulSize

例)

LPVOID g_lpvStartPoint;
INT g_iIndexCount;

INT m_iCurrentStatus;
INT m_iPrevStatus;

LPCTSTR lpctszFilePath;
LPBYTE lpbyBuffer;

■■パスカル記法

 パスカル記法は、各単語の先頭を大文字にして連結した命名規約である。後述のキャメル記法の対向的な存在として表現されることが多いが、Windows C/C++開発のメインの記法の1つである。

 このパスカル記法の概要は以下のようになる。

  1. 一貫して単語の先頭は大文字で始める。
  2. 単語の間に記号は使用しない。

例)

CurrentStateProducer.cpp
PrivateDataGenerator.cpp

int MaxStatus;
long MaxKeywords;

■■キャメル記法

 キャメル記法は、Java開発における標準の命名規約である。Java言語だけでなく、C#でも一部はキャメル記法をルールとしている。この記法の特徴は、「IDEの変数名補完機能を使用する際、SHIFTキーすらも使わずに済む。」「動詞や形容詞を先頭に持ってくることで、設定するのか/取得するのか キーワードで関数名候補が絞られて表示できる」という現代のIDEの機能を引き出すための記法でもある。

 このキャメル記法の概要は以下のようになる。

  1. 変数先頭の単語は小文字で始まる。
  2. 以降の単語の先頭は大文字となる。
  3. ハンガリアン記法のような型を表す修飾子を使用しない。
  4. 関数名の場合、先頭は動詞、以降は名詞を使用する。
  5. 変数名の場合、先頭は形容詞、以降は名詞または形容詞を使用する。

例)

int currentStatus; // 現在のステータス
int previousStatus; // 以前のステータス
short minSelectValue; // 最小の選択値
short maxCounter; // 最大のカウンタ値

int getCurrentStatus() { return currentStatus; };
void setPreviousStatus(int value) { previousStatus = value; };
short calcMinSelectValue();
short clearMaxCounter();

■■スネーク記法

 スネーク記法は、Linux開発、特にシェルスクリプトにて見られる命名規約である。C言語では、#defineなどの定義名に使用される。定数定義では大文字が使用され、変数や関数名では小文字が使用されるパターンがほとんどである。

 このスネーク記法の概要は以下のようになる。

  1. 単語と単語の間にアンダーバー '_' を使用して連結する。
  2. 単語に大文字を使用しない。
  3. 大文字と小文字を混同しない。

例)

int previous_current_status;
long current_file_pos;

#define MAX_FILE_SIZE 256
#define DATA_SIZE 1024

■ソリューション名

 「ソリューション」はVisual Studioにおけるモジュール(成果物)の一括作成単位の名称である。システム全体の名称となるため、名称が長くなる場合は、そのシステム名の「コードネーム」などを検討し、適切な長さの名称となるようにする。

■プロジェクト名

 「プロジェクト」は1つのバイナリモジュールを作成する単位の名称である。プロジェクトの成果物は、以下を指す。

  • 単体で実行可能なバイナリモジュール
  • 上記のバイナリモジュールから利用される、共有モジュール
  • バイナリモジュールにコンパイル済みロジックを提供する共通ライブラリ
  • 上記モジュールを自動でテストするテストプロジェクト

 上記のプロジェクトを生成するプロジェクト名は、以下の規約を用いる。

  1. 共有モジュール名の最後には、サフィックスとして"Library"をつける。
  2. 共有モジュールで、上記の"Library"の機能を制御する事を目的としたものは、サフィックスに"Control"をつける。
  3. 単体実行可能なモジュールのサフィックスに、"Driver"をつける。
  4. 上記のプロジェクトのテストプロジェクトにはサフィックスに、"Test"をつける。

例)

Alternate  
├ConsoleDriver  
├XamlDriver  
├MiddleControl  
├WindowsLibrary  
├WindowsLibraryTest  
├OSSLibrary  
└OSSLibraryTest

名前空間名規約

 名前空間はこれから作成する機能を、プロジェクトをまたいでグルーピングする際のグループ名に当たる。STL(Standard Template Library)は広範囲に高機能なライブラリを提供するが、使用される名前空間は、"std"にまとめられる。Boostライブラリも同様に"boost"にまとめられる。

 このように、名前空間は複数のプロジェクトをまたいで統一した名前を提供することが望ましい。また、名前空間名はコーディングの煩雑さを軽減するためにも3文字程度に留める事を推奨する。

 名前空間入れ子で作成することも可能であるが、上記同様コーディングの煩雑さを持ち込むことになるので、階層構造を十分に吟味して採用する必要がある。

例)

alt  
├alt::db  
│├alt::db::Oracle  
│├alt::db::SqlServer  
│└alt::db::SqLite3  
├alt::file  
│├alt::file::csv  
│└alt::file::txt  
├alt::process  
│└alt::process::priority  
└alt::thread  
 └alt::thread::priority  

 データベース名などは、メジャーな名前なのでコーディング時に思いつきやすいが、それ以外は全機能を把握しないとIDEの名前補完機能の恩恵を受けられない。名前空間の階層化は、名前空間の全体設計とプロジェクト構成、ファイル構成のすべてを検討する必要がある。また、"std::"や"boost::"のように単一名前空間提供で問題ない場合は、採用すべきである。

 データベースプログラムは、ベンダー別にプログラムした部分と、それらを抽象化して実際に利用する部分を明確に分けることで、抽象化コードと具現化したコードが混在していることを明確化することができる。この場合、抽象化が不足していることが原因であり、コードを見直す必要を名前空間から判断することができる。

■ヘッダーファイル規約

 ヘッダーファイルの構成は以下の順に宣言を記載する。

  1. ファイルヘッダーコメント
  2. #pragama once
  3. 空白
  4. #include <...>
  5. 空白
  6. #include "..."
  7. 空白
  8. #define定義群
  9. 空白
  10. const 宣言群
  11. 空白
  12. 構造体定義群
  13. 空白
  14. クラス定義群(本来は独立したファイルにクラスの定義を行う)
//------------------------------------------------------------------------------
// sample機能を実装したファイル
//------------------------------------------------------------------------------
#pragma once

#include <stdio.h>
#include <stdlib.h>

#include "BaseDefine.h"

#define POS position

const int __maxFile 10;
const char* __baseFilePath "C:\\Work"

■ソースファイル規約

 ソースファイルの構成は、以下の順に実装を行う。

  1. ファイルヘッダーコメント
  2. 空白
  3. #include "これから定義を行うヘッダーファイル"
  4. 空白
  5. 関数定義
//------------------------------------------------------------------------------
// sample機能を実装したファイル
//------------------------------------------------------------------------------

#include "sample.h"

void sample::sample()
{
…

■定数定義規約(#define)

 定数の宣言は、以下のルールに準拠する。

  1. 名前規約はスネーク記法(大文字)とする。
  2. できるだけ先頭に形容詞を設けて、どの値を定数宣言しているのかを明確する。

例)

#define MAX_CONFIG_FILE_PATH 256
#define MIN_SEQUENCE_DATA_LENGTH 12

■定数定義規約(constグローバル定義)

 定数の宣言は、以下のルールに準拠する。

  1. グローバル変数ルールの"__"をプレフィックスに付ける。
  2. ハンガリアン記述で変数型をプレフィクスに付ける。

例)

const int __iMaxSequenceCount = 750;
const char* __lpctszDefaultConfigFilePath = ".\\config\config.ini";

■クラス・構造体名/ファイル名規約

 クラス・構造体名は、「パスカル記法」を使用して作成する。以下の機能に該当する場合は、プレフィックスサフィックスルールに留意する事。

  • クラス名のプレフィックスに’C'または'c'を付けない。
  • 構造体名のプレフィックスに’S'または's'を付けない。
  • 広範囲な機能をインスタンス生成不要で提供する場合、サフィックスに"Utility"をつける。
  • 広範囲な機能をインスタンス生成を前提に提供する場合、サフィックスに"Helper"をつける。
  • 仮想関数のみで構成され、Javaのインターフェースに相当する機能を提供する場合、プレフィックスに"I"をつける。
  • 上記のインターフェースに対する、機能の実装を行うクラスの場合、プレフィックスに"I"を、サフィックスに"Impl"をつける。
  • 自動テストを実装する場合、サフィックスに"Test"をつける。
  • ファイル名はそれを内包するクラス、構造体の名称を使用する。
  • 複数のクラス・構造体を1つのファイルに同梱しない。ただし、Strategyなどの密接なクラス構成を1つのファイルにまとめる(他の機能で使われることがない)場合はこの限りではない。

■■public, protected, privateキーワード

 クラスのスコープ(public, protected, private)と、スコープに所属する関数、変数は、以下の順序で宣言する。クラスの関数宣言時に関数本体も実装する事が可能であるが、その関数が1行(セミコロン1つ)で実装できる場合を除き、クラス内に関数を実装する事は避ける事。

public宣言

  1. デフォルトコンストラクタ宣言
  2. 引数ありコンストラクタ宣言
  3. コピーコンストラクタ宣言
  4. デストラクタ宣言
  5. 継承が必要となる関数宣言
  6. その他のpublic関数宣言

protected宣言

  1. 隠蔽するコンストラクタ宣言
  2. 継承が必要となる関数宣言
  3. protected関数宣言
  4. protected変数宣言

private宣言

  1. 継承が必要となる関数宣言
  2. private関数宣言
  3. private変数宣言

■コンストラク

 コンストラクタは実装方法が複数あるため、当規約では以下のスタイルを推奨する。コンストラクタで、処理の実装を行うことは禁止とする。コンストラクタが戻り値を渡せないため、処理の成否を確認できないためである。また、継承元クラスがある場合は、その継承元クラスのどのコンストラクタを呼び出すのかを明確にする。

ExtendObject::ExtendObject(int initialValue)
    : BaseObject(initialValue) // 引数付き継承元コンストラクタを指定する。
{
    this->Init(); // 初期化関数を使用する。
}

bool ExtendObject::Init()
{
    // 内部のメンバー変数を初期化する。
    // Init()単体で使用することで、再初期化が可能となる。
}

■デストラク

 デストラクタは実装するクラスが継承されない場合、"virtual"をつけない。実装するクラスが継承される場合、"virtual"を必ずつける。

■その他のpublic関数

 コンストラクタ、デストラクタ以外に以下の関数の実装を検討する。

  • クラス内の変数を初期化するためのInit()関数
  • クラスの終了時に使用したヒープ領域やファイルのクローズを行うExit()関数

■関数名規約

 関数名は、スコープに関わらずパスカル記法で記載する。キャメル記法のように先頭は動詞で以降は名詞を使用する。

 できる限り、KatakanaJikkou()のようなローマ字表記を行わず、英単語を使用する。

■変数名規約

■■グローバル変数

 グローバル変数プレフィックスとしてアンダーバー2つを使用する。

 そもそもであるが、グローバル変数以外の実装方法(スコープの限定化)を検討する。main()の内部は、グローバル関数と生存期間がほぼ同じである。うまく引数を使って、グローバル変数の宣言を使わずに済む方法を検討する。

■■クラス変数

 クラスのインスタンス変数、クラス変数のプレフィックスとしてアンダーバー1つを使用する。この規約により、IDE名前補完機能からクラスのインスタンス変数を探す場合は、アンダーバーを入力することで、一覧表示されるようになる。なお、インスタンス変数とクラス変数を区別するような規約は設けない。

■その他の規約

現時点では、記載事項なし。

キャメル記法について

キャメル記法はJava用だとは思うけど。

昨今の、コード補完機能を持つIDEで開発する際は、とても便利な規約となる。

問題なのは、英語が得意でない日本人が、「動詞」だの「形容詞」だのそこまで深く理解していない言語で、キャメルを考えようとするから、おかしなものばかりが増えていくことに。

■キャメル記法によるメソッド名の命名規約

キャメル記法でメソッド名を考える場合、先頭は「動詞」となり、以降は名詞、形容詞のパスカル形式になる。先頭の動詞が大切。

例)

void getCurrentDirectory(std::string& currentDirectory);
bool isValidDataPath(std::string& checkPath);

基本的には、「○○する」なので、習ってきた英語の範囲で何とかなりそう。問題なのはI/Oの方向は分かるのだが、表現が多いから結局どれが妥当なのか判断が付かない事。例えば、アウトプットする言葉で連想できるのは、export, out, output, …など。

■■対になるサンプル

  • set 設定する
  • store 格納する
  • write 書き込む
  • output 出力する

などと、なるが「そこにwriteを使う?」という過ち(というか、読む人の主観)も発生する。あくまでイメージだが、以下の言葉と対応する格納先でどうだろうか。

単語 格納先 サンプル 対向
set 変数 setValidNo() get
write ファイル writeCurrentResult() read
store データベース storeResponseData() load
output コンソール outputErrorLog() input
send ネットワーク sendCompressedPacket() recv, receive

■■単独になるサンプル

日本語英語サンプル
計算するcalccalcAverageTemperature()
追加するaddaddFilePath()
探すfindfindInvalidChar()
searchsearchInvalidChar()
置換するreplacereplaceOutputPath()
取り除くremoveremoveAbstractPath()
検証するverifyverifyInputAddress()
checkcheckInputAddress()
決定するapplyapplyUserInput()
decidedecideUserInput()
否決するrejectrejectUserInput()
denydenyUserInput()

■■データベースなどのSQLに対応するメソッド名はそのままSQLライクに作る。

動作 英語 サンプル
取得 select selectCustomer()
追加 insert insertCustomer()
更新 update updateCustomer()
削除 delete deleteCustomer()

■■true/falseを返すメソッドに関しては以下のルールが適用できるか検討する。

動作 英語 サンプル
これは○○○か? is isCurrentData()
これは○○○を持っているか? has hasOddNumber()
○○○ができるか? can canVerifyData()

■■あまりにも名前が短くて、キャメル形式になれないもの

動作 英語 サンプル
コピー copy copy()
移動 move move()
削除 delete delete()

■キャメル記法による変数名の命名規約

変数名は先頭が形容詞となり、以降は形容詞、名詞が続くことになる。先頭の形容詞が重要である。

例)

int currentTemp; // 現在の温度
int previousTemp; // 前回の温度
int averageTemp; // 平均の温度
形容詞 英語 サンプル
現在の current currentRow
前の previous previousRow
最大の max maxValue
最小の min minValue
中央の median medianValue
平均の average averageValue

データベースのフィールド名などは名詞をスネーク記法で表現しているものがあるため、キャメル記法ルールをあきらめなければならない。逆にスネーク記法に出会ったら、それはDBフィールド名であると判断することができるため、混在とも言えないと思われる。

コーディングスタイル:波括弧の位置

自分の信念となる「コーディングスタイル」を持っていますか?

信念というよりは、両端を揃えた名刺のような心地よさとでも申しましょうか。
日本人ですから、揃えることは美学であり、哲学でもあります。

カッコの位置

■ formatted by Eclipse 2020-02

Eclipse 2020-02のフォーマッターは数種類ありますが、選択肢によって以下のように変わっていきます。

■■ Active profile:K&R [built-in]

波括弧の始まりは、行末(右側)に置く事を良しとするフォーマットスタイル。
Javaは頑として、波括弧は行末スタートだったと記憶しています。
そしてC++の御先祖様になる、Cの標準的なスタイルはこれだったかなと。

/*
 * A sample source file for the code formatter preview
 */
#include <math.h>

class Point {
public:
    Point(double x, double y) :
            x(x), y(y) {
    }
    double distance(const Point &other) const;

    double x;
    double y;
};

double Point::distance(const Point &other) const {
    double dx = x - other.x;
    double dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
}

■■ Active profile:BSD/Allman [built-in]

波括弧の始まりは、行頭(左側)に置く事を良しとするフォーマットスタイル。
波括弧の開始と終了が同一カラムに存在することになるから、私としては
統一感があっていいような気がしています。

/*
 * A sample source file for the code formatter preview
 */
#include <math.h>

class Point
{
public:
    Point(double x, double y) :
            x(x), y(y)
    {
    }
    double distance(const Point &other) const;

    double x;
    double y;
};

double Point::distance(const Point &other) const
{
    double dx = x - other.x;
    double dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
}

■■ Active profile:GNU [built-in]

これは、上記のインデントが2になったものに見えます。
HTMLやCSSではないので、インデント2は読みづらい。

/*
 * A sample source file for the code formatter preview
 */
#include <math.h>

class Point
{
public:
  Point (double x, double y) :
      x (x), y (y)
  {
  }
  double
  distance (const Point &other) const;

  double x;
  double y;
};

double
Point::distance (const Point &other) const
{
  double dx = x - other.x;
  double dy = y - other.y;
  return sqrt (dx * dx + dy * dy);
}

■■ Active profile:Whitesmiths [built-in]

行頭(左側)に波括弧を配置することになるが、インデントが加わっている。
カッコ=スコープの意味合いが薄くなった気がします。

/*
 * A sample source file for the code formatter preview
 */
#include <math.h>

class Point
    {
public:
    Point(double x, double y) :
        x(x), y(y)
    {
    }
    double distance(const Point &other) const;

    double x;
    double y;
    };

double Point::distance(const Point &other) const
    {
    double dx = x - other.x;
    double dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
    }