おがさわらなるひこのオープンソースとかプログラミングとか印刷技術とか

おがさわらなるひこ @naru0ga が技術系で興味を持ったりなんだりしたことをたまーに書くブログです。最近はてなダイアリー放置しすぎて記事書くたびにはてな記法忘れるのではてなブログに移行しました。

クリエイティブ・コモンズ・ライセンス
特に断りがない場合は、本ブログの筆者によるコンテンツは クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスの下に提供されています。

短期集中連載? LibreOfficeをWindowsで開発してみよう:その⑤ 不具合直して? みたよ

GWの間にLibO開発できる環境をWindowsで作ろうの連載5回目。さてGWやらも本日で終わり、なのでこの短期集中連載もさしあたりの最終回。

過去記事:第1回 第2回 第3回 第4回

せっかく開発環境作ったのになんにも触らないのもアレなんで、適当なバグを直してみようと。ターゲットにしたのはこれ。

bugs.documentfoundation.org

この不具合なんですが、平たく言うとExcelで作成した「セルの書式設定で大字(12345……を壱弐参四五……って書くやつ)を指定したセルが、LibO Calcで開くと大字にならないってものです。

Excelで大字表示ってどうやるんだろ……って調べてみると、たとえばこのサイトがヒットしたんですけど:

www.kenzo30.com

書式記号に [DBNum2] って指定すればいいらしいです。

Excelの書式指定子(Modifier)、Calcでも指定できるので指定してみる……と、

f:id:naruoga:20200506210339p:plain
書式指定子 [DBNum2] 指定

ホントだ。大字にならないですね。ということでこれを追ってみることにします*1

ソースを検索して該当部分を探す

最初はやみくもにソースを検索して該当部分を探してみましょう*2

ソースの検索は OpenGrok でも git grep でもよいですが、まあせっかくなのでVS2019上で検索してみることにします。 Ctrl-Shift-F を押してソリューション内検索ダイアログを開いて "DBNum2" を検索します。

見つかったのはテストコード、コメントを除くと /svl/source/numbers/zformat.cxx だけ。名前からいってもそれっぽい。ということで中を覗いてみます。

デバッガを使いつつなんとなく眺める

パッと目に入るのは、 SvNumberformat のコンストラクタ。 こいつもまあ相当長いメソッドなのですが、要は書式指定子の文字列をパースして [DBNum2] なら 2 を取り出して保持しておく処理っぽいですね。

あと DBNum というキーワードでなんとなくコードを眺めると:

sal_uInt8 SvNumberNatNum::MapDBNumToNatNum( sal_uInt8 nDBNum, LanguageType eLang, bool bDate )
sal_uInt8 SvNumberNatNum::MapNatNumToDBNum( sal_uInt8 nNatNum, LanguageType eLang, bool bDate )

というペアのメソッドが目につきます。なにやら内部的に DBNum と NatNum なるものを相互に読み替えているらしい。 試しにデバッガで前者にブレークポイント設定して*3 DBNum2 を設定してみたら呼ばれてますね。なるほど。 私はこんな風にブレークポイントしかけて確かにこれが見るべき処理かって確認するのよくやります。 机上で追い込むの苦手なので……。

そこで、おっと思ったのが、さっきのテストでセルの書式設定に [DBNum2] とやったやつ。 あれを保存して再度読み込みなおすと、 [NatNum4] という書式指定に代わってしまう。

f:id:naruoga:20200506215618p:plain
書式指定が変わってしまう

ここでようやっと、仕様確認しようと思いつきました(遅い)。ヘルプを "DBNum2" で検索すると、このページがヒットしました。

help.libreoffice.org

「Displaying Numbers Using Native Characters」とあるとおり、NatNum というのはNative Numberなんでしょうね。 LibOの書式指定のネイティブはこっち。

で、Excel(OOXML)からインポートしたり、手打ちで書式指定 DBNum2 とかした場合は、適切なNatNumに変換して内部的にはそれを使って処理する……と。なるほどね。 エクスポートするときには逆に NatNum から DBNum に書き戻すのでしょう。

さてヘルプを見ると、…… DBNum2NatNum4 に対応するということですね。 それは、ODSで保存して読み込み直すときの結果と符合しますね。なのでここに間違いはないと*4

じゃあそうなると、書式指定をもとに実際の表示文字列を組み立てるところ、それが問題あるということでしょうか。 表示文字列を組み立てるところ、それって多分ですけど、普通の実装なら、 セルの内容を渡したら文字列が返ってくるメソッドを用意するんじゃないでしょうかね*5

ということで、ソースコードgrepして(ここはVS2019使わないんかい!) zformat.cxx の中で戻り値が OUString なものをgrepして、中のぞいて、あーそれっぽいと思ったのがこの二つ。

OUString SvNumberformat::impTransliterateImpl(const OUString& rStr,
                                              const SvNumberNatNum& rNum ) const
{
    css::lang::Locale aLocale( LanguageTag( rNum.GetLang() ).getLocale() );
    return GetFormatter().GetNatNum()->getNativeNumberStringParams(rStr, aLocale, rNum.GetNatNum(),
                                                                   rNum.GetParams());
}

void SvNumberformat::impTransliterateImpl(OUStringBuffer& rStr,
                                          const SvNumberNatNum& rNum ) const
{
    css::lang::Locale aLocale( LanguageTag( rNum.GetLang() ).getLocale() );

    OUString sTemp(rStr.toString());
    sTemp = GetFormatter().GetNatNum()->getNativeNumberStringParams(
        sTemp, aLocale, rNum.GetNatNum(), rNum.GetParams());
    rStr = sTemp;
}

どっちも GetnativeNumberStringParams() なるメソッドを読んでるわけですが、ここはVS2019でステップ実行でステップインして飛びますと、 最終的には i18npool/source/nativenumber/nativenumbersupplier.cxxNativeNumberSupplierService::getNativeNumberString() メソッドに来るわけですね。

で、こいつをデバッグ実行しつつ追うと、

    switch (nNativeNumberMode)
    {
        ...
        case NativeNumberMode::NATNUM4: // Text, Lower, Long
            number = &natnum4[langnum];
            break;

という処理があって、ここで number という変数に入ってくるのは、すでに大字ではない通常の漢数字……ふむぅ。……ん?

ここで仕様(というかヘルプ)を再度確認

はい、私のよくなかったところ。 おもむろにソースを見る前に、「解くべき問題は果たして何か(何が正しいふるまいか)」を確認する必要がありましたね。

で、ヘルプ再度確認すると、いやー間抜け:

f:id:naruoga:20200506230700p:plain
NatNum4 は modern long Kanji text !

NatNum4 は modern だって書いてあるじゃー、ないですか!

なのでヘルプの記述が仕様として正しいなら、今の動作は完全に正しい。そして、古いヘルプ:

help.libreoffice.org

を見ると、この動作は3.3から……というかOpenOffice.orgのころから変わっていない、と思われます。

でもおかしいですよね、Excelでは DBNum2 は大字なのだとしたら、当然LibOではそれに対応する NatNum5 じゃないと、バグ起票されたようにExcelとの互換性がないということになってしまう。うーん。わからん。

目をつぶって直す

まあいいや、もし仮にこいつ直すとしたらどうするか。

結果としては DBNumXNatNunX の割り当てだけが問題ってことになったので、前述の:

sal_uInt8 SvNumberNatNum::MapDBNumToNatNum( sal_uInt8 nDBNum, LanguageType eLang, bool bDate )
sal_uInt8 SvNumberNatNum::MapNatNumToDBNum( sal_uInt8 nNatNum, LanguageType eLang, bool bDate )

この二つを直せばいいということになります。

本来ならすべてのマッピングを見直す方が適切なのかもわからないですが、さしあたりはこの問題だけ直してみます。 以下diffだけだと若干情報足りないので手で補った嘘diffです。

@@ -170,7 +170,7 @@ sal_uInt8 SvNumberNatNum::MapDBNumToNatNum( sal_uInt8 nDBNum, LanguageType eLang
          switch ( nDBNum )
          {
          ...
          case 2:
             if ( eLang == primary(LANGUAGE_CHINESE))
                 nNatNum = 5;
             else if ( eLang == primary(LANGUAGE_JAPANESE) )
-                nNatNum = 4;
+                nNatNum = 5;
             else if ( eLang == primary(LANGUAGE_KOREAN) )
                 nNatNum = 2;
             break;

こちらは DBNum2NatNum4 にして、

@@ -238,7 +238,7 @@ sal_uInt8 SvNumberNatNum::MapNatNumToDBNum( sal_uInt8 nNatNum, LanguageType eLan
          switch ( nNatNum )
          {
          ...
          case 4:
             if ( eLang == primary(LANGUAGE_CHINESE) )
                 nDBNum = 2;
             else if ( eLang == primary(LANGUAGE_JAPANESE) )
-                nDBNum = 3;
+                nDBNum = 2;
             break;
         case 6:
             if ( eLang == primary(LANGUAGE_CHINESE) )

こちらは逆に NatNum4DBNum2 に戻しているだけ。あー、超簡単だ。

で、この変更を入れてビルドしたやつで、バグ報告についてたファイルを開いてみると……。

f:id:naruoga:20200506233209p:plain
6.4だと大字になってない

f:id:naruoga:20200506233429p:plain
直したやつだとちゃんと大字になる

直ってますね!

もし……仮にもし、これをパッチに出すとすると、

  • まずはあるべき仕様を日本コミュニティで議論(メーリングリストかな?)。
  • 上記に基づき、DBNum2 以外に対する処理も書く。
  • ユニットテストでこの処理を確認しているものがあるげなので、直すなり、もしこのケースを確認してないなら追加するなりする。
  • ヘルプも直す。
  • コミット整えて、gerrit に push

ってな感じですかねー。

まあともかく、開発環境作ってリアルな問題を(少なくとも技術的には)解けるところまではいったので、 ゴールデンウイークとしてはまあまあの成果かな?

せっかくなので、これっきりにしないで定期的に開発にかかわっていきたいですね!

*1:なお調査しなくてもhimajin100000さんがコメントで書いてくださってるんですが、それはなんというか、練習なので、カンニングしない方向で……。

*2:慧眼なる読者の皆様ははこれが誤った手順であることにお気づきかと思います……。詳細はのちほど。

*3:個人的な言葉づかいで「ブレークポイントをはる」っていうんですが、この「はる」は張るなのか貼るなのかそれ以外なのか。

*4:もう気付いている人はいらっしゃると思いますがこれも伏線……です。

*5:引数で渡したオブジェクトの参照に値詰めて戻る実装もあるけど、文字列なら普通returnします……よね?