syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

printfとString.Format

(これは2013-06-27に書いた故OCNブログの記事を移植したものです)

.NETのString.Format()やConsole.Write()/WriteLine()の書式を駆使すれば、ほぼCのprintf系をカバーできるんですが、MSDNの書式指定構文の説明にある、

% [flags] [width] [.precision] [{h | l | ll | I | I32 | I64 }]type

のうちの[flags]はちょっと工夫が要りそうなのでメモ。以下、C/C++C#の対比。

printf("%6.2f", 12.345); // " 12.35"

Console.Write("{0,6:F2}", 12.345);

でいけるのだが、

printf("%+7.2f", 12.345); // " +12.35"

に直接相当する書式がなさげ。なので、

static string FormatWithPlusSign(double val, string precisionFormat)
{
  // いったん左詰めで文字列化する。
  return (!Double.IsNaN(val) && val > 0 ? "+" : "") + val.ToString(precisionFormat);
}

というメソッドを定義して、

Console.Write("{0,7}", FormatWithPlusSign(12.345, "F2"));

という感じ。

いわゆる単項のプラスというヤツですが、あえて+記号を入れることで正負の符号の対比がよく分かるようになるので、自分はプログラムコード中でベクトルや行列を定義したり、それらを文字列フォーマットしたりするときにしばしば使います。
例えば、

float3 dir1 = {+1, 0, 0};
float3 dir2 = {-1, 0, 0};

という正規ベクトルを出力するとき、

printf("v1 = (%+.1f, %+.1f, %+.1f)\n", dir1.x, dir1.y, dir1.z);
printf("v2 = (%+.1f, %+.1f, %+.1f)\n", dir2.x, dir2.y, dir2.z);

としたほうが、対比がよく分かると思いませんか? いわゆるリーダブルコードというヤツです。

ちなみに式の中で単項のプラスを許可しないプログラミング言語は少数派で、LuaとかHaskellだけみたいです。

なお、Visual C++によるWindowsストア アプリ開発で使えるWinRTランタイムのPlatform::Stringには、ATL/MFCCStringや、.NETのSystem.Stringと違ってFormat()メソッドがなくて超不便なんですが、なぜかというとPlatform::Stringは実はCOM実装で、cdeclの可変個引数が使えないのが主な理由です。あと文字列操作のパフォーマンスもstd::stringやstd::wstringに比べて劣るため、そういう意味ではCOMのBSTRラッパーである_bstr_tクラスと大差ないです。文字列書式化はstd::stringstream/wstringstreamとか、boost::format()/wformat()でもいいんですが、ATLが使えるVS 2012 ProもしくはVS 2013/2015 Pro/Community環境であれば、平常時はATL::CStringWを使って、WinRTとの境界面だけにPlatform::Stringを使うのがよいでしょう。

2016-08-08追記:
ToString()の引数に「セクション区切り記号」を使うことで、FormatWithPlusSign()メソッドの代わりになるようです。

Console.Write("{0,7}", (12.345).ToString("+#.00;-#.00;0.00"));

しかし、System.Double.PositiveInfinityは「+∞」ではなく「∞」とだけ表示されました。残念……
ちなみにこういったBCLのちょっとしたテストは、Visual Studioに統合されているF#/C# Interactiveのような対話環境を使うと非常に効率的です。