はじめに
ご存知のとおり、UiPath.Excel.Activities パッケージは自動化プロジェクトで Excel ファイルや CSV ファイルを取り扱う場合に使うパッケージです。UiPath.Core.Activities パッケージ以外で最も使用頻度の高いパッケージと言っても過言ではないでしょう。
Excel ファイルを取り扱う場合、大きく二つの方法があります。ひとつは App Integration>Excel カテゴリに属するアクティビティを使う方法です。もうひとつは System>File>Workbook カテゴリに属するアクティビティを使う方法です。いずれのアクティビティも UiPath.Excel.Activities パッケージに含まれます。
※本内容はv2018.3.1にて検証した内容となります。
ROUND 関数の問題
2018年10月現在、UiPath.Excel.Activities パッケージの System>File>Workbook カテゴリに属する Excel ファイルを読み込む Read 系アクティビティを使うと、ROUND 関数を含む式の計算値が期待値と異なる場合があることが確認されています。
例:
セル「=ROUND(0.5,0)」が 0 を返します。ROUND 関数は、第 1 引数で渡された数値を四捨五入する関数です。第 2 引数がゼロの場合、小数点以下で四捨五入します。実際に Microsoft Excel アプリケーションで読んだ場合、このセルの値は 1 となります。
この問題は、UiPath Studio バージョン2018.2.5に含まれる UiPath.Excel.Activities バージョン 2.3.6682.26635 等で確認されています。
実装に関する情報
実は、UiPath.Excel.Activities パッケージの App Integration>Excel カテゴリに属するアクティビティは、Microsoft Excel が提供している COM コンポーネント:Microsoft.Office.Interop.Excel を利用して実装されています。この COM コンポーネントは Microsoft Excel がインストールされた環境で利用可能です。したがって、UiPath.Excel.Activities パッケージの App Integration>Excel カテゴリに属するアクティビティも Microsoft Excel がインストールされた環境でのみ正しく動作します。逆に Microsoft Excel のない環境ではエラーを発生して使えません。
一方、UiPath.Excel.Activities パッケージ(バージョン 2.4、2.3、2.0)の System>File>Workbook カテゴリに属するアクティビティは、ClosedXML パッケージが提供する機能を使って実装されています。Microsoft Excel の有無に関係なく使えるわけです。
ClosedXML はオープンソースプロジェクトです。ソースコードは GitHub にて公開されています。(https://github.com/ClosedXML/ClosedXML)
検証
方針
前述の実装情報より、今回の問題が発生する Excel 関数の計算は ClosedXML パッケージに含まれるプログラムコードにおいて行われると考えられます。
その原因を探るにあたり、公開されているソースコードを地道に読むという選択肢もありますが、それには多少の時間がかかると予想します。時間短縮のため、使用する ClosedXML パッケージのバージョンを上げて、Excel 関数が返す値が変化するかを観察する実験を行ってみたいと思います。
入力データ
読み込み対象の Excel ファイル input.xlsx をワークフロープロジェクトのフォルダーに配置します。そして、このファイルのシート名 Sheet1、セル A1 に
=ROUND(0.5,0) |
を書き込んでおきます。
ワークフロー
次のワークフロー ReadCellTest を用意します。
ClosedXML バージョン 0.87.1 による結果
今回実験に使用する UiPath Studio はバージョン 2018.2.5 です。この Studio は、ClosedXML バージョン 0.87.1 を同梱しています。
では、前述のワークフローを実行し、出力パネルの結果を見てみましょう。
出力値は 1 が期待されましたが、結果は 0 でした。確かに問題が再現しました。
ClosedXML バージョン 0.88.0 を使う準備
次のパッケージファイルを Studio のインストールフォルダーの Packages フォルダー(ローカルフィード)に管理者権限でコピーします。
ローカルフィードにコピーするファイル | 備考 |
---|---|
ClosedXML.0.88.0.nupkg | https://www.nuget.org で入手してください。 |
DocumentFormat.OpenXml.2.7.2.nupkg | |
FastMember.Signed.1.1.0.nupkg |
DocumentFormat.OpenXml パッケージと FastMember.Signed パッケージは、ClosedXML パッケージのバージョン 0.88.0 が依存しているパッケージです。
Studioに戻り、Activitiesパネル上のManage Packagesアイコンをクリックしてパッケージ管理ウィンドウをオープンします。
左側のカラムのAvailableのLocalを選択し、(①)
Filter Activitiesのチェックを外し、(②)
中央のカラムの検索フィールドに「closedxml」と入力し、(③)
「ClosedXML」を選択し、(④)
右側のカラムのAvailableが0.88.0.0であることを確認し、(⑤)
Update ボタンをクリックします。(⑥)
以上で準備が整いました。
ClosedXML バージョン 0.88.0 による結果
では、ClosedXML パッケージのバージョンを 0.88.0 に上げて出力値がどう変わるかを観察してみましょう。
Studioを再起動して、前出のワークフローを実行し、出力パネルの結果を見てみましょう。
出力値は 1 です。成功です。期待した結果が出ました。
考察
前述の検証結果は次のとおりです。
ClosedXML パッケージのバージョン
|
=ROUND(0.5,0)の値
|
---|---|
0.87.1 | 0 |
0.88.0 | 1 |
ClosedXML パッケージのバージョン 0.87.1 とバージョン 0.88.0 でいったい何が違うのでしょうか。
ソースコードの差分を取ると、検出されたものから次の部分を見つけられました。
diff -ruN ClosedXML-0.87.1/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs ClosedXML-0.88.0/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs --- ClosedXML-0.87.1/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs 2018-10-17 16:01:22.371411100 +0900 +++ ClosedXML-0.88.0/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs 2018-10-17 16:05:18.961670100 +0900 @@ -1,4 +1,4 @@ - using System; + using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -470,13 +470,13 @@ var digits = (Int32)(Double)p[1]; if (digits >= 0) { - return Math.Round(value, digits); + return Math.Round(value, digits, MidpointRounding.AwayFromZero); } else { digits = Math.Abs(digits); double temp = value / Math.Pow(10, digits); - temp = Math.Round(temp, 0); + temp = Math.Round(temp, 0, MidpointRounding.AwayFromZero); return temp * Math.Pow(10, digits); } |
このコードは、ClosedXML.Excel.CalcEngine 名前空間の MathTrig クラスのスタティックメソッド Round からのものです。
ご覧の通り、MathTrig.Round メソッドでは、戻り値の計算に System 名前空間の Math.Round メソッドを使っています。Math.Round メソッドは文字通り渡された数値を丸めた値を返します。注目すべきは中間値の場合の丸め方です。それは MidpointRounding 列挙子の指定に左右されます。
バージョン 0.87.1 では、MidpointRounding 列挙子を指定しない形で Math.Round メソッドを呼び出しています。この場合、MidpointRounding.ToEven を指定した場合に相当し、丸める数値が中間値の時に戻り値は最も近い偶数となります。例えば、(0.5,0) は 0 を返し、 (1.5,0) は 2 を返します。
バージョン 0.88.0 では、MidpointRounding.AwayFromZero を指定した形で Math.Round メソッドを呼び出すように変更されています。この場合、丸める数値が中間値の時に戻り値は最も近い整数でゼロから離れている整数となります。例えば、(0.5,0) は 1 を返し、(1.5,0) は 2 を返します。
つまり、MathTrig.Round メソッドが返す値は、バージョン 0.87.1 で見られた問題のある値とバージョン 0.88.0 で見られた正しい値と完全に合致します。
偶然の一致でしょうか。いいえ、そうではないと思います。
MathTrig.cs を読むと、MathTrig クラスの Register メソッドで Excel 関数の評価時に呼び出すメソッドの登録をしていることが分かります。そこで ROUND 関数の評価時に呼び出すメソッドにこの Round メソッドが登録されています。
以上の結果から、この MathTrig.Round メソッドが今回の問題の発生源であると言ってよいでしょう。この差分から、バージョン0.88.0とそれ以降のバージョンでは Excel 関数が返す値と同じ値=正しい値が返されることが予想されます。
ROUND 関数以外の Excel 関数について考えると、MathTrig クラスが持つメソッドの実装が大きく影響することが容易に想像できます。Excel 関数の実装が公開されているわけではないため、その外部仕様に基づいてあるいは Excel 関数の実際の挙動にあわせて MathTrig クラスの実装がメンテナンスされているのではないかと思われます。したがって、Microsoft Excel でファイルを開いた場合と完全な互換性があると断言するのは難しいかもしれません。比較的新しいものや使用頻度の少ないものなど、サポートしていない Excel 関数もあるのではないでしょうか。
おわりに
今回の問題は ClosedXML バージョン 0.87.1 が持つ ROUND 関数の計算ロジックが引き起こしている問題であると分かりました。この問題については、ClosedXML パッケージをバージョン 0.88.0 以降にアップグレードすることにより解決できることも分かりました。それと同時に、ClosedXML パッケージの限界 ~ Microsoft Excel の提供する機能と 100% の互換性を保つことが難しいことも分かりました。(なお、本記事は UiPath.Excel.Activities パッケージがその将来バージョンで ClosedXML パッケージを使い続けることを何ら保証するものではないをご理解ください。)
以上より導き出された教訓は次のとおりです。
Excel ファイルを取り扱う場合、Microsoft Excel がインストールされている環境においては必ず App Integration>Excel カテゴリに属するアクティビティを使うべきです。それにより互換性の問題に直面することはないでしょう。特にファイルが開かれた時点で評価される式がセルに格納されている場合には、Excel アプリケーションスコープで関連アクティビティを使うことを強く推奨します。
UiPath.Excel.Activities パッケージの System>File>Workbook カテゴリに属するアクティビティは、Microsoft Excel を使用せずにExcelファイルを取り扱えるため、便利であることについては間違いありません。式評価における互換問題が発生する可能性があることを十分に理解した上で賢く使いたいですね。