ホーム | プログラミングTClock |


2004/02/02記。02/10修正。

TClock Lightの書式処理

ここでは、TClock Lightでの書式処理、つまり "yy/mm/dd ddd" のような文字列を "04/02/01 月" のように変換している部分について解説します。

TClock Lightでは、旧TClock、TClock2chと同様に、書式用文字列を頭から1文字ずつ調べ、出力先のバッファを1文字ずつ埋めていきます。ただし、ソースを簡潔にして拡張しやすくするために、書式文字を見つけたら対応する関数を呼び出して処理させるという方式にしています。

TClock Lightの書式処理はポインタ使い過ぎで、「読みにくい」あるいは「しょぼい」と思われるかもしれませんが、処理速度、メンテしやすさ、私の力量、の妥協点は、まあこんなもんです。旧TClockのような長大なwhileループよりはマシと言えましょう。

関数 MakeFormat

TClock Lightで書式を処理しているのは、dll/format.cの関数 MakeFormat です。この関数は、時計を表示するとき、ツールチップを表示するとき、指定の書式でクリップボードにコピーするときに呼ばれます。

void MakeFormat(wchar_t* dst, const SYSTEMTIME* pt,
    const wchar_t* pfmt, int nMax)

引数は次のようになります。

TClock Lightの書式では、旧TClock、TClock2chとは違い、文字列をユニコードとして扱います。ユニコードとC言語のワイド文字(wchar_t)は別物ですが、Windows APIでは同じものとして扱っても大丈夫です。

MakeFormat の内容

関数 MakeFormat の中身を見ていきます。まず、引数 pt が NULL だったら、API GetLocalTime で現在の時刻を取得します。NULLでなければ、 st に引数 ptの中身をコピーします。引数 pfmt が NULL だったら、外部変数 m_format を指すようにします。

    SYSTEMTIME st;
    FORMATHANDLERSTRUCT struc;
    int i;
    
    if(pt == NULL) GetLocalTime(&st);
    else memcpy(&st, pt, sizeof(SYSTEMTIME));
    
    if(pfmt == NULL) pfmt = m_format;

書式処理用の構造体 FORMATHANDLERSTRUCT struc のメンバ変数をセットします。struc.dp は 出力先のポインタの位置、struc.sp は書式文字列のポインタの位置、struc.pt は SYSTEMTIME へのポインタです。

    struc.dp = dst;
    struc.sp = pfmt;
    struc.pt = &st;

出力先である dst の中身を半角スペースで埋め、最後にヌル文字を付けます。これは、dst がバッファオーバランしないようにするための目印です。

    for(i = 0; i < nMax-1; i++) dst[i] = ' ';
    dst[i] = 0;

書式文字列を1文字ずつ調べ、出力先を埋めていくループに入ります。TClock2chと互換性を取るため、<% 〜 %> で囲まれた部分の書式だけを処理し、それ以外はそのまま出力先に文字をコピーします。

文字を処理するたびに、struc.dp(出力先のポインタの位置)とstruc.sp(書式文字列のポインタの位置)を進めていきます。出力先があふれないように、常に *struc.dp がヌル文字かどうかをチェックします。

    while(*struc.sp && *struc.dp)
    {
        if(*struc.sp == '<' && *(struc.sp + 1) == '%')
        {
            struc.sp += 2;
            while(*struc.sp && *struc.dp)
            {
                if(*struc.sp == '%' && *(struc.sp + 1) == '>')
                {
                    struc.sp += 2;
                    break;
                }
                
                /* ここで書式を処理する */
            }
        }
        else
        {
            *struc.dp++ = *struc.sp++;
        }
    }
    *struc.dp = 0;

上記の /* ここで書式を処理する */ の部分です。" 〜 " で囲まれた部分は書式文字列を出力先にそのままコピーします。

                if(*struc.sp == '\"')
                {
                    struc.sp++;
                    while(*struc.sp != '\"' && *struc.sp && *struc.dp)
                        *struc.dp++ = *struc.sp++;
                    if(*struc.sp == '\"') struc.sp++;
                }

次が、書式処理の中心部分です。配列 format_handers (後述)を1つずつ調べ、

書式の1文字が一致するか(*struc.sp == format_handers[i].ch)、

書式の文字列が一致したら(wcsncmp(struc.sp, format_handers[i].prefix, wcslen(format_handers[i].prefix)) == 0)、

書式処理用の関数 func に構造体 struc を渡して呼び出します(format_handers[i].func(&struc);)。

                else
                {
                    for(i = 0; i < NUM_HANDLERS; i++)
                    {
                        if(*struc.sp == format_handers[i].ch ||
                            (format_handers[i].prefix &&
                              wcsncmp(struc.sp, format_handers[i].prefix,
                                 wcslen(format_handers[i].prefix)) == 0))
                        {
                            format_handers[i].func(&struc);
                            break;
                        }
                    }
                    if(i == NUM_HANDLERS)
                        *struc.dp++ = *struc.sp++;
                }

配列 format_handers

format.c の頭では、構造体の配列 format_handers を初期化しています。構造体の中身は、書式文字(ch)、書式文字列(prefix)、書式処理用関数へのポインタ(func)、です。

関数 MakeFormat では、文字 '/' にぶつかったら関数 SDateHandler を呼び出し、文字列 "aaa" にぶつかったら関数 DayOfWeekHandler を呼び出す、ということになります。

typedef void (*HANDLERFUNC)(FORMATHANDLERSTRUCT* pstruc);

struct {
    wchar_t ch;
    wchar_t* prefix;
    HANDLERFUNC func;
} format_handers[] =
{
    { '/', NULL, SDateHandler },
    { ':', NULL, STimeHandler },
    { 'y', NULL, YearHandler },
    { 'm', NULL, MonthHandler },
    { 'd', NULL, DateHandler },
    { 0, L"aaa", DayOfWeekHandler },
    (中略)
};

#define NUM_HANDLERS (sizeof(format_handers) / sizeof(format_handers[0]))

自分で新しい書式を追加したい場合は、書式用文字と書式処理用関数を決めます。たとえば、"X" や "XX" という書式を関数 XHandler に処理させたいときは、配列 format_handers に次のように追加します。

    { 'X', NULL, XHandler },

"HOGE" という書式を関数 HogeHandler に処理させたいときは、配列 format_handers に次のように追加します。

    { 0, L"HOGE", HogeHandler },

書式処理用の関数

書式処理用の関数は、次のような形です。引数は必ず FORMATHANDLERSTRUCT* で、戻り値はなしです。

void 何々Handler(FORMATHANDLERSTRUCT* pstruc);

構造体 FORMATHANDLERSTRUCT は次のようになります(dll/tcdll.h)。dp は 出力先のポインタの位置、sp は書式文字列のポインタの位置、pt は SYSTEMTIME へのポインタです。

typedef struct {
    wchar_t* dp;
    const wchar_t* sp;
    SYSTEMTIME* pt;
} FORMATHANDLERSTRUCT;

例として、dll/formattime.c の中の関数 MinuteHandler を載せます。MinuteHandler は、書式が n だったら1桁または2桁の分の数を、nn だったら2桁の分の数を出力します。

void MinuteHandler(FORMATHANDLERSTRUCT* pstruc)
{
    int min = (int)pstruc->pt->wMinute;
    int keta = 1;
    pstruc->sp++;
    if(*pstruc->sp == 'n') { keta++; pstruc->sp++; }
    if(keta == 2 || min > 9)
    {
        if(*pstruc->dp)
            *pstruc->dp++ = (wchar_t)(min / 10 + '0');
    }
    if(*pstruc->dp)
        *pstruc->dp++ = (wchar_t)(min % 10 + '0');
}

書式処理用の関数でやることは次のとおりです。

次は、書式 tt を処理する 関数 AMPMHandler です。午前/午後を表す文字列を出力します。時の数を調べて午前だったら外部変数 m_AM、午後だったら m_PM の中身をコピーします。

void AMPMHandler(FORMATHANDLERSTRUCT* pstruc)
{
    const wchar_t* p;
    
    pstruc->sp += 2;
    if(pstruc->pt->wHour < 12) p = m_AM; else p = m_PM;
    while(*p && *pstruc->dp) *pstruc->dp++ = *p++;
}

午前/午後、曜日、月名、元号、などの文字列は、dll/formattime.c の関数 InitFormatTime であらかじめ外部変数に入れてあります。