2024/10/21

Notes - Excel 連携:#42)PlotArea とサイズ

前回から、”使える”グラフにするための操作をまとめています。今回は具体的なコーディングに入る前に、グラフの作画エリアの挙動を確認します。


作画エリアの操作

Excel のグラフを作画しているエリアは VBA で PlotArea オブジェクトとして管理されています。オブジェクトの取得は Chart オブジェクトの PlotArea プロパティから取得します。

Chart.PlotArea プロパティ (Excel)

PlotArea オブジェクト (Excel)


作画エリアのサイズ調整は PlotArea オブジェクトのプロパティから行います。上記リンク先を眺めるとサイズにかかわるプロパティが 2 セット存在します。

Height InsideHeight
Left InsideLeft
Top InsideTop
Width InsideWidth

それぞれの違いについて理解する必要がありそうです。ヘルプを確認すると、次の通り記載されていました。グラフ領域とグラフの端、プロット領域の違いが不明瞭ですね...

Left グラフ領域の左端までの距離
InsideLeft グラフの端からプロット領域の左端までの距離


Left と InsideLeft の違い

ヘルプだけではよくわからないので、各プロパティの値をメッセージボックスで確認します。あわせて、軸エリアの幅も表示させてみました(以下の画像は合成して作成)。

InsideLeft がおおよそ 24、軸エリアの幅が 23 となっています。この結果より、InsideLeft には軸エリアを含んだ値であることがわかりますので、Inside* プロパティが指し示すエリアは下図の赤枠の部分で、ヘルプで言う ”プロット領域” だと推察できます。

Left や Width が指すのは、軸エリアを含む緑枠のエリアのようです。ただ、今一つ不明瞭なので点線としています。

少なくともX軸とY軸の交点を固定するためには、Inside* プロパティを使えはできそうですね。


幅の操作

プロット領域の位置を調整するテストをしていて設定どおりにならないことが、何度もありました。これは位置の設定はグラフ領域を超えないからです。

例えば、事例に上げているグラフでは、グラフ領域はほぼエリアいっぱいに設定されています。この状態で Left プロパティに 100 をセットしても、実行後の Left の値は 18 となりました。デフォルトの状態で右側にあったマージンの分だけ、右側に移動したということだと思います。

このように範囲外の数値を設定した場合、エラーも発生させず、適当な値に設定されます。希望通り配置できないことがあれば、各プロパティの値を表示させ確認しないと、バグなのか計算間違いなのか判定しづらくなります。

私は、通常 Pixel で考えるのですが、これらプロパティの単位はすべて Point ととなります。これも、状況を複雑にさせる要因ですね。Excel の単位については、以前まとめたので以下のリンクをご確認ください。

#35)Excel で使用する単位と変換


位置合わせの謎

次の実験は、Inside* プロパティで位置をセットしたグラフを画像にした場合、希望通りの位置に配置されるかを確認します。

テストとしては、InsideWidth = 400 にセットしてから、InsideLeft = 50 にセットします。グラフ領域のサイズが 500 なので、中央に配置する算段です。

その結果は次の通りでした。

いい感じなのですが、微妙に右にずれているような気がします。そこで、このグラフをペイントにコピペして、サイズをチェックします。

すると次の通りでした。

やはり右にずれていますね。プロット領域の 535 ピクセル は 401.25 ポイントなので、おおむね指定したサイズになっています。左の 72 ピクセルは 54 ポイントとなり指定通りとなっていません。

試しに、設定後のプロパティを表示したところ、50 ポイントとなっています。

前述のテストで不明瞭だった Left の値は約 27 となっており、軸エリアの幅を足すと InsideLeft と合致します。よって、Left に起因する問題ではなさそうです。


現時点では残念ながら原因は不明でした。少し右にずれるという点を認識して先に進めます。原因が判明したら別途レポートさせていただきます。


まとめ

今回は、プロット領域 PlotArea について調査しました。一部不明瞭な点がありましたが、軸をそろえることは今回の調査の範囲で実現できそうです。

次回からは、この情報を活用しつつ、実際にコーディングしていきます。


前回 DXL Step-by-Step


2024/10/18

Notes - Excel 連携:#41)グラフの調整

この連載の #11 ~ #19 で Notes から Excel のグラフを作成する方法を紹介しました。そして、前回まで 3 回にわたり紹介した名前アイコンの生成では、画像ファイルとして保存できるようになりました。これらを組み合わせると、LotusScript だけでノーツのデータをグラフ化できます(裏で Excel は使用しますが...)。

そして、別の連載 DXL Step-by-Step の『#41)インラインイメージの貼り付け』を利用すれば、作成したグラフを見える状態(インラインイメージ)で貼りつけることができます。ここまでできるようになると、別の次元のノーツ文書が作成できるようになりますね。


グラフの調整が必要となるシーン

ただ、これまでに紹介したグラフの作成方法は、Excel が自動で行ってくれる機能を多分に利用しています。これでもそれなりに美しいグラフは作成できるのですが、業務で実用しようとすると問題点が出てきます。

例えば、複数のグラフを作成して比較する場合です。#18 のプログラムは 30 日分のユーザ数の推移をグラフにするサンプルでした(下図の上のグラフ)。これを修正して Y 軸の桁数が違うグラフを作成して並べてみます(下図の下のグラフ)。すると、X 軸と Y 軸の交点の位置がずれてしまいます。これでは比較しづらく、レポートして及第点はもらえないですよね。

今回からしばらくは、この問題の解決を題材とします。具体的には、グラフの軸ラベル、タイトル、凡例などを制御して、”使える”グラフにするための操作をまとめます。


作成するグラフ

今回チャレンジするグラフのイメージは次の通りです。

まずは、メインの目的であるグラフエリアのサイズを固定します。

左には軸ラベルを表示するエリアを含め一定の幅を確保します。タイトルと凡例はグラフ右に集め、表示するエリアとして、こちらも決められた幅を確保します。これらは位置はグラフエリアを最大化して値の変動を確認しやすくするのが狙いです。


前回 DXL Step-by-Step 次回


2024/10/13

DXL Step-by-Step:#41)インラインイメージの貼り付け

DXL 活用の調査・検証で、実現できたことや発見したことご紹介する『DXL Step-by-Step』シリーズの第 41 回です。しばらく表の話が続いたので少し流れを変えます。

今回はリッチテキストに直接貼り付ける画像についてまとめます。 ノーツクライアントでは、下図のようにプロパティで見るとソース欄に[インラインイメージ]と表示されるパターンですね。

DXL を使えば、リッチテキストに画像を貼った文書を作成することができます。また、文書から画像をダウンロードしてファイルに保存することも可能です。


インラインイメージの DXL

先ほどのリッチテキストを DXL で書き出すと次のようになります。

画像を表す png タグがあります。その配下には暗号化されたような文字列がテキストノードとして大量に出力されています。この構造は『#8)イメージリソース(設計要素)の DXL』で紹介した、イメージリソース内に保存されている画像データと同じですね。

画像データの1つ上は picture ノードとなっています。このノードは『#26)イメージリソースの DXL』で、リッチテキストにイメージリソースを配置する際に出てきました。

リッチテキストに配置したインラインイメージとイメージリソースの違いは、配下のノードが画像データか、リソースの参照 imageref かだけです。


サンプルプログラム

それでは、実際にインラインイメージを配置するプログラムを作成します。『#27)イメージリソースの表示』で作成したプログラムをベースに作業します。

エージェントをコピペして xSetDXL_ImageResource 関数を次の通り修正します。

Function xSetDXL_InlineImage(vddn As NotesDOMDocumentNode, vdenRT As NotesDOMElementNode, ByVal vsFileName As String)
   Dim denPar As NotesDOMElementNode
   Dim denPic As NotesDOMElementNode
   Dim den As NotesDOMElementNode
   Dim dtn As NotesDOMTextNode

   Dim nst As NotesStream
   Dim iType As Integer
   Dim iSizeX As Integer
   Dim iSizeY As Integer

   '画像ファイルの確認
   Set nst = xns.CreateStream()
   Call nst.Open(vsFileName)
   iType = xGetImageFileInfo(nst, iSizeX, iSizeY)

   If iType <> DXL_Image_Unknown Then

      '段落の作成
      Set den = vddn.CreateElementNode("par")
      Call den.SetAttribute("def", "1")
      Set denPar = vdenRT.AppendChild(den)

      '画像の作成
      Set den = vddn.CreateElementNode("picture")
      Call den.SetAttribute("width", CStr(iSizeX) & "px")
      Call den.SetAttribute("height", CStr(iSizeY) & "px")
      Set denPic = denPar.AppendChild(den)

      'インラインイメージの作成
      Set den = vddn.CreateElementNode(xGetImageNodeName(iType))
      Set den = denPic.AppendChild(den)

      '画像データセット
      Set dtn = vddn.CreateTextNode(StreamToBase64(nst))
      Call den.AppendChild(dtn)
   End If
End Function


画像ファイルの確認

xSetDXL_InlineImage 関数には、引数 vsFileName を追加しました。これはインラインイメージとして貼り付ける画像のファイル名です。指定したファイルは、関数の最初で画像の形式を確認しています。

画像ファイルを調査して、画像のフォーマットとサイズを返す関数 xGetImageFileInfo をコールしています。この関数は、『#13)イメージの形式とサイズの取得』で紹介しています。xGetImageFileInfo とサブ関数 xGetImageFileType をコピペします。


画像の作成

picture ノードの作成では、画像のサイズ width と height 属性を追加しています。イメージリソースを呼び出す場合はサイズ指定がなくても表示されたのですが、インラインイメージの場合は指定しないと画像が表示されませんでした。


インラインイメージの作成

インラインイメージの場合、画像の形式により作成するタグが変わります。画像ファイルに合致したタグ名を xGetImageNodeName 関数から取得し、ノードをを作成しています。

xGetImageNodeName 関数は次の通りです。

Function xGetImageNodeName(ByVal viImageType As Integer) As String
   Dim s As String

   If viImageType = DXL_Image_PNG Then s = "png"
   If viImageType = DXL_Image_GIF Then s = "gif"
   If viImageType = DXL_Image_JPEG Then s = "jpeg"

   xGetImageNodeName = s
End Function


画像データセット

画像データは Base64 でエンコードされた文字列を指定する必要があります。変換する関数 StreamToBase64 は『#10)イメージリソースの新規作成』で紹介しているので、コピペします。


呼び出し元の修正

xSetDXL_InlineImage 関数が完成したら、呼び出しもとを修正します。

Function xSetDXL(vdprs As NotesDOMParser)
         ・・・
   '段落定義
   Call xSetDXL_pardef(ddn, denRT)

   'インラインイメージの追加
   Call xSetDXL_InlineImage(ddn, denRT, "E:\411.PNG")
End Function

これでサンプルは完成です。実行するとこの記事の最初の画像のようにインラインイメージが貼り付けられたリッチテキストが作成されます。


まとめ

今回はインラインイメージをリッチテキストに追加する方法を紹介しました。LotusScript 標準の RichText クラスではできない機能ですので、これができるようになるとプログラム経由で作る文書の表現力が格段にアップしますね。


前回 DXL Step-by-Step


2024/10/11

DXL Step-by-Step:#40)表のスタイル - タブ形式

ノーツの表には特殊な設定があって、これを利用するとタブ形式やアコーディオン形式の UI が作成できます。表のプロパティでは[表の行]タブの設定になります。今回はこの設定を DXL で操作してみましょう。

タブ形式の表では、表の行数分のタブが表示され、タブをクリックすることでその行を表示します。要はタブのクリックで表示する行を切り替える機能ということですね。ですので、プロパティでは『1 行のみ表示』を選択することになります。

そして、スタイルは『行の表示方法』で指定します。『タブボタンで行を選択』でタブ形式、『表題で行を選択』でアコーディオン形式となります。タブの表題は『タブラベルと表題』にしています。これは、行ごとに設定することになります。


表のスタイルの設定

DXL で表のスタイルを設定するには、table ノードの rowdisplay 属性を設定します。これまでこの属性を指定していなかったので、通常の表として表示されていたということですね。

属性 設定値 補足
rowdisplay (なし) 通常の表
'tabs' タブボタンで行を選択(タブ形式)
'captions' 表題で行を選択(アコーディオン形式)


タブの表題の設定

タブの表題は行ごとに指定することから tablerow ノードの属性として設定します。属性名は tablabel です。


サンプルプログラム

#32)表の作成と行・列の間隔 では、3 行 2 列の単純な表を作成するサンプルプログラムを紹介しました。今回はこれを改造します。

まず、表を作成する関数 xSetDXL_table を修正します。赤字が修正箇所です。

Function xSetDXL_table(_
            vddn As NotesDOMDocumentNode, _
            vdenRT As NotesDOMElementNode, _
            vvTabName As Variant, ByVal viCols As Integer _
            ) As NotesDOMElementNode
   Dim denTbl As NotesDOMElementNode
   Dim denRow As NotesDOMElementNode
   Dim den As NotesDOMElementNode
   Dim iCol As Integer
   Dim iRow As Integer

   '表の作成
   Set den = vddn.CreateElementNode("table")
   Set denTbl = vdenRT.AppendChild(den)
   'タブ形式の表
   Call denTbl.SetAttribute("rowdisplay", "tabs")

   '列定義の作成
   For iCol = 1 To viCols
      Set den = vddn.CreateElementNode("tablecolumn")
      Call denTbl.AppendChild(den)
   Next

   '行の作成
   For iRow = 0 To UBound(vvTabName)
      Set den = vddn.CreateElementNode("tablerow")
      Set denRow = denTbl.AppendChild(den)
      'タブ名称
      Call denRow.SetAttribute("tablabel", vvTabName(iRow))

      'セルの作成
      For iCol = 1 To viCols
         Set den = vddn.CreateElementNode("tablecell")
         Call denRow.AppendChild(den)
      Next
   Next

   Set xSetDXL_table = denTbl
End Function

まず、引数では行数を指定するのではなく、タブの表題を文字列型の配列で渡す仕様に変えています。これに伴い、行を作成するループの指定が配列の要素数分に変更されています。

そして、本題のタブの設定は、table ノードに rowdisplay 属性、tablerow ノードに対して tablabel 属性をセットしている 2 か所となります。


関数の修正に伴い、呼び出しもとを次のように修正します。

Function xSetDXL(vdprs As NotesDOMParser)
         ・・・
   '表前の段落追加
   Call xSetDXL_par(ddn, denRT)

   'タブ表の追加
   Dim asTabName(1)
   asTabName(0) = "Sample Tab 1"
   asTabName(1) = "タブ2"
   Set denTbl = xSetDXL_table(ddn, denRT, asTabName, 1)


   '間隔の設定
         ・・・
End Function

タブ名称を asTabName 配列にセットして、関数をコールしているだけです。


実行結果

修正したプログラムを実行すると、次のようにタブが 2 つの表が作成されます。

また、rowdisplay 属性を captions に変更すると次のようにアコーディオン形式となります。


前回 DXL Step-by-Step


2024/10/09

リスト配列

先日、LotusScript でリスト配列を使う機会がありました。初めての利用だったのでヘルプで調べながら開発を行いました。せっかく調べたので、機能を整理しておきます。


リスト配列とは?

LotusScript では、Boolean、Integer、String などさまざまなデータ型が存在します。そして、同じデータ型の値をまとめて管理する構造体があります。配列というとなじみがありますよね。List も配列の一種です。

通常の配列は a1(0) など変数の後ろにカッコでインデックス番号(添え字や要素番号とも言います)を付けると値にアクセスできます。一方、List の場合は、インデックス番号の代わりに文字列(以下リストタグと呼ぶ)を使用します。

例えば、ユーザとそのユーザの点数を管理する場合を考えてみましょう。それぞれ、次のようなコードとなります。

List の場合 配列の場合
aiScore("Ito") = 75
aiScore("Sato") = 90
asName(0) = "Ito"
asName(1) = "Sato"
aiScore(0) = 75
aiScore(1) = 90


宣言方法

リスト配列を宣言するには次のように記述します。

   Dim aiScore List as Integer

aiScore("Ito") と配列のように記述するのですが、宣言に ( ) は必要ありません。要素や領域の確保は不要ということですね。また、配列のように多次元にすることはできません。


値の追加方法

リスト配列に値を追加するのは簡単です。次のように要素にリストタグを指定するだけです。

    aiScore("Ito") = 75

リスト配列内に "Ito" という要素がなければ新規で追加され、値がセットされます。すでに存在した場合は、値が更新されます。配列のように ReDim 操作が不要なので便利ですね。


値のアクセス

リスト配列の値のアクセスも値との追加と同じように、リストタグを指定するだけです。例えば、以下のコードはリストタグ Ito の値を 10 加算しています。

    aiScore("Ito") = aiScore("Ito") + 10


ループ

平均値を出力するなど、リスト内のすべての要素にアクセスしたい場合があります。このような処理には Forall ループを使用します。

例えば、次のコードではリスト配列 aiScore の合計 iSum と 件数 iCnt を算出し、平均を表示しています。

   ForAll iScore In aiScore
      iSum = iSum + iScore
      iCnt = iCnt + 1
   End ForAll

   MsgBox "平均点 = " & Format(iSum / iCnt, "#0.0")

Forall で使用している iScore がリスト配列の各要素(値)が順に入る変数です。


リスト配列の各種操作

上記情報で、リスト配列の通常使用は可能だと思います。ここから先はリスト配列利用に関連する各種操作をまとめます。チェックなど込み入った操作をする場合などに参考にできます。


◇ リストタグの取得

Forall ループの処理では、各要素の値がループ変数にセットされました。その値のリストタグを取得する関数が ListTag 関数です。

次のコードではリスト配列 aiScore のすべてのリストタグを順に表示します。

   ForAll iScore In aiScore
      MsgBox ListTag(iScore)
   End ForAll


◇ 要素の存在確認

続いては指定したリストタグが、リスト内に存在するか判定する方法です。次のコードではリスト配列 aiScore に Tanaka の要素があるか判定します。

   If IsElement(aiScore("Tanaka")) Then
      MsgBox "リスト配列内に存在します!"
   Else
      MsgBox "リスト配列内に存在しません。"
   End If


◇ 要素の削除

Erase ステートメントを使用するとリスト内の要素を削除することができます。

   Erase aiScore("Hata")

もし、指定した要素がリスト配列内に存在しない場合、エラーが発生します。


◇ リスト配列の初期化

Erase ステートメントを使用するとリスト配列内のすべてのエントリを削除し初期化することができます。

   Erase aiScore

リストタグを指定せず変数名を指定するとすべての要素を削除するということですね。


◇ リスト変数の判定

DataType 関数を使用すると変数のデータタイプが取得できます。

   Dim aiScore List As Integer
   MsgBox DataType(aiScore)

リスト配列は 2048 となりますが、上記の結果は 2050 となります。これは 整数型のリスト配列のためで、リスト配列 2048 + 整数型 2 = 2050 となります。型により値が変わるので注意が必要です。

また、リスト配列かを判定するだけであれば IsList 関数が便利です。

   If IsList(aiScore) Then
      MsgBox "aiScore はリスト配列です。"
   Else
      MsgBox "aiScore はリスト配列ではありません。"
   End If


◇ 空の判定

リスト配列を宣言した直後や Erase で初期化した後は、中身が空っぽの状態となります。この判定の方法については明確な関数は発見できませんでした。Ubound や Lbound のような関数はないようです。また、残念ながら IsEmpty の戻り値は True となり判定できませんでした。

Forall ループでは、ループ内の処理が一切実行されないので、それで判定する方法が良いかもしれませんね。


まとめ

今回は、リスト配列の使い方についてまとめました。配列と同様に、同じデータ型の値をまとめて管理できる変数でした。要素のアクセスは リストタグ と呼ばれる文字列で指定する点がポイントですね。

この記事の最初に『ユーザとそのユーザの点数を管理する場合』の例を上げ、配列との比較をしました。どちらが優れているという話をしたかったわけではありません。配列とリスト配列、近しい機能ではありますが、それぞれ違いがあります。作成するプログラムの仕様に合わせて、適材適所、使い分けができるといいですね。


2024/10/06

意外と難しかった四捨五入の作成

前回 は Round という関数は四捨五入ではなかったという話をしました。アプリ開発では必要となるシーンがありますので、ないのなら作ってしまいましょう。

ということで、今回は四捨五入する処理の作成を行います。そのサンプルを紹介するだけのつもりだったのですが、想定外の障害に出くわし、思いのほか難航しまた。そこで、その問題点も含めてまとめておきます。


LotusScript の場合

まずは、簡単に解決できる LotusScript での処理です。Round という関数が存在するので RoundOff 関数としました。

引数は Round 関数と同じで、丸めたい値 vdNum と 丸める桁 viPlaces です。

Function RoundOff(Byval vdNum As Double, Byval viPlaces As Integer) As Double
   Dim dDigit As Double
   Dim d As Double

   '丸めたい桁を1の位にする係数を作成
   dDigit = 10 ^ viPlaces

   '丸めたい桁を1の位にする
   d = vdNum * dDigit

   '四捨五入(0.5 を加算して切り捨て)
   d = Int(d + 0.5)

   '桁を戻す
   RoundOff = d / dDigit
End Function

コメントを記述しているので解説は不要かと思います。ポイントは四捨五入の代わりに 0.5 を足して、少数を切り捨てている点です。


式言語で問題発生

続いては@関数の四捨五入を作成してみます。@Round に倣って、丸めたい値 xNumber と 丸める桁 xFactor を用意して演算させます。

式の構成は上記 LotusScript の関数と揃えています。ただ、丸める桁の値の持たせ方が違いますので、乗除記号が逆転している点に注意してください。

   xNumber := 0.25;
   xFactor := 0.1;

   REM {丸めたい桁を1の位にする};
   xTmp := xNumber / xFactor;

   REM {四捨五入(0.5 を加算して切り捨て)};
   xTmp := @Integer(xTmp + 0.5);

   REM {桁を戻す};
   xTmp * xFactor

式ができ上がって検証しているときに事件が発生しました。なぜか、0.350 の場合に切り捨てが発生してしまいました !?

xNumber の値 0.249 0.250 0.251 0.349 0.350 0.351
実行結果 0.2 0.3 0.3 0.3 0.3 0.4

式をいくら見直しても原因がわかりません。そこで、式の途中経過を順に確認しました(数値フィールドの計算結果として確認)。

検証した式 結果(0.250) 結果(0.350)
xNumber / xFactor 2.5 3.5
xNumber / xFactor + 0.5 3 4
@Integer(xNumber / xFactor + 0.5) 3 3

@Integer に原因があることが明白になりました。そこで、Google 先生に聞いてみたところ以下のリンクが見つかりました。

@Integer 関数で小数を扱った場合の動作について

リンク内の事例でも @Integer の結果が 1 少なくなる場合があることが示されています。今回もそれが原因なのでしょう。

回避策を探して Workaround を確認すると、@Round を使えと書いてあります。@Round の四捨五入が使えないから @Integer を頼ったのに...。頭が混乱してきました。


このページの一番最後にこの現象の原因解説のリンクがありました。

浮動小数点演算と丸め誤差について

細かな話は分かりませんが、少数の値は 2 進数で表すと有限桁数で表せない場合があり、それに起因する誤差によるものだそうです。


式言語の四捨五入

少数の値とその誤差が問題原因なのであれば、できる限り少数を使わなければ抑制できるかもしれません。今回の事例では、丸めたい値が少数なのは仕方がないとして、それ以外の少数利用を避けてみようと考えました。

具体的には次の部分、誤差が混入しうる値(要は小数値)を演算に使用している点に着目しました。

   xTmp := xNumber / xFactor;


対策は次の通りです。

丸める桁の値の持たせ方を LotusScript の Round と合わせて、次のように記述しました。これで、元の値以外は整数(もしくは整数部)となります。

   xNumber := 0.350;
   xPlaces := 1;

   REM {丸めたい桁を1の位にする係数を作成};
   xDigit := @Power(10; xPlaces);

   REM {丸めたい桁を1の位にする};
   xTmp := xNumber * xDigit;

   REM {四捨五入(0.5 を加算して切り捨て)};
   xTmp := @Integer(xTmp + 0.5);

   REM {桁を戻す};
   xTmp / xDigit

サポート情報にある Workaround では誤差を @Round でごまかす方法を取っていましたが、誤差が混入しにくくすることで回避できないか挑戦してみるという方法ですね。


結果は 0.250 であっても 0.350 であっても正しく四捨五入できました。念のため、さまざまな値や桁数を変えてテストしましたが、問題はなさそうでした。


まとめ

今回は、四捨五入の処理を@関数と LotusScript で作成してみました。@関数では想定外のトラブルに見舞われ、思いのほか時間がかかってしまいました。ただ、問題の原因がわかれば、症状が出にくいように対策すれば回避できる事例になればと思い、紹介しました。


原因となった『@Integer 関数で小数を扱った場合の動作について』なのですが、この現象がいつから発生しているのかは記載がありません。原因からするとはるか昔から延々と受け継がれているような気がします。そして、対応のステータスは Deferred となっています。要は対応する気がないのだと理解しました。

ノーツは 30 年以上前のプログラムがそのまま動作する互換性が高い製品です。これがノーツをビジネスで利用する上で、重要な利点のひとつだと思っています。

だからと言って、バグまで互換性維持する必要はないですよね...。せっかくの利点が悪しき文化にならないように祈ります。


2024/10/05

四捨五入にご用心!

今回は、@関数と LotusScript の丸め処理、なかでも四捨五入についてまとめます。Notes の世界で四捨五入するには Round という関数を使用すると理解していましたが、結果的には ”四捨五入” ではない部分がありました。順に確認しましょう。


@Round 関数

式言語で丸め処理のを行う関数に @Round があります。

@Round (式言語)

説明には『指定された数値を最も近い整数に四捨五入します。』とあります。そして、構文は以下の通りとなっています。

   @Round( number ; factor )

2 つ目の引数 factor が丸める桁を指定します。0.1 と指定すると少数 1 桁の値に丸めます。


LotusScript の Round

Round 関数は LotusScript にも存在ます。

Round 関数 (LotusScript 言語)

説明には『値を指定した小数で丸めます。』とあり、構文は次の通りです。

   Round ( numExpr , places )

@Round と同様に 2 つ目の引数が丸める桁の指定です。ただし、『目的の小数点以下桁数を表す数式』となっています。少数 1 桁の値に丸めたいときには 1 と指定することになります。


丸め処理の不思議

2 つの関数を紹介しましたが気になることはありませんでしたか?

そうなんです。@Round は ”四捨五入” となっていますが、Round は ”丸め” となっています。これが翻訳の問題なのか、機能が違うのか確認しましょう。

@関数と LotusScript で少数 1 桁の値に丸めるプログラムを作成します。数値 x の値を変えながら動作を観察します。すると次のようが結果が出ました。

x 0.249 0.250 0.251 0.349 0.350 0.351
@Round(x; 0.1) 0.2 0.3 0.3 0.3 0.3 0.4
Round(x, 1) 0.2 0.2 0.3 0.3 0.4 0.4

いかがですか?

どちらも四捨五入ではありませんでした。しかも、ちょうど 5 の時の処理は @関数は”奇数”になるように調整、LotusScript は”偶数”となるように調整していました。

なぜ、このような違いになっているのか不思議ですね...


まとめ

今回の検証で、Round は、@関数、LotusScript とも四捨五入ではないことがわかりました。これら関数を利用する際には特性を理解した上で使用しましょう。正しい結果が得られない可能性がありますので...。

ちなみに、ノーツの四捨五入について調査していると以下のリンクに出くわしました。

LotusScript での四捨五入について

本家のサポート情報で、LotusScript で四捨五入を行う関数は用意されていないので Evaluate で @Round を実行する方法が紹介されています。今回の検証結果によるとうまくいかないはずなのですが...。

注意しましょう!


2024/10/03

@Text と Format

数値や日付データを文字列に変換する方法についてまとめます。特に書式を指定して変換する関数として、@関数では @Text、LotusScript では Format があります。これら関数は、使用頻度も高く便利なのですが、多機能な分、引数の指定が複雑です。ついついヘルプを見てしまう関数ですね。そこでよく使う変換をまとめておきます。


構文の確認

まずはそれぞれの関数の構文を確認します。

@関数の @Text は次の通りです。

   @Text( value ; format-string )

LotusScript の Format 関数は次の通りです。

   Format[$] ( expr [ , fmt ] )

引数の順は同じですね。どちらも 2 つ目の引数で変換する書式を文字列で指定する仕様となっています。今回はこの引数の指定方法についてまとめることになります。


数値の場合

数値を文字列に変換する際に行う処理としては、小数点以下の桁数をそろえる、3 桁ごとにカンマで区切る(カンマ編集)、一定の桁数に満たない場合 0 で埋める(ゼロ埋め)があります。それぞれ下記の通りです。

1234.567 という値の変換についてまとめました。

変換結果 @Text の書式 Format の書式 補足
1235 F0 0 整数のみ
1234.6 F1 0.0 少数 1 桁
1234.57 F2 0.00 少数 2 桁
1,234.6 F,1 #,##0.0 カンマ編集
01235 (なし) 00000 ゼロ埋め

@Text ではゼロ埋めする指定がないようなので、式で記述する必要がありそうです。

表示用の計算結果フィールドの式の例です。SourceNumber フィールドの値をゼロ埋めしています。xNumber に変換する値、xDigit に桁数を代入します。

   xNumber := SourceNumber;
   xDigit := 5;

   REM {エラー処理};
   @If(xNumber = ""; @Return(""); @Success);

   REM {指定した桁数でゼロ埋め};
   @Right(@Repeat("0"; xDigit) + @Text(xNumber; "F0"); xDigit)

なお、この式は桁あふれの場合、注意が必要です。例えば 1234567 の場合、"34567" となります。LotusScript の Format では "1234567" となります。


日付/時刻の場合

日付/時刻データでは、@関数と LotusScript で対比できるほど近しい指定が少ないので別々に記載します。

LotusScript の場合

Format 関数の引数の指定は単純です。年 月 日 時 分 秒 のそれぞれの英単語の頭文字を使って指定します。ただし、月 と 分 は同じ m となるため、分 では n を使用します。

また、1 文字ではゼロ埋めなし、2 文字でゼロ埋めとなります。これもわかりやすいですね。

例えば、2024/1/2 3:04:05 の場合、変換結果と引数の文字列は次のようになります。

変換結果 Format の書式 補足
1/2 m/d 月日(ゼロ埋めなし)
2024/01 yyyy/mm 年月(ゼロ埋め)
2024/01/02 yyyy/mm/dd 年月日(ゼロ埋め)
3:04 h:nn 時分(時はゼロ埋めなし)
3:04:05 h:nn:ss 時分秒(時はゼロ埋めなし)
2024/01/02 03:04:05 yyyy/mm/dd hh:nn:ss 年月日時分秒(ゼロ埋め)


@関数の場合

@Text 関数で日付/時刻値に対して指定できる記号は次のようなものがあります(抜粋)。

書式記号 補足
D0 年月日
D1 月日、現在の年でなければ年
D2 月日
D3 年月
T0 時分秒
T1 時分
S0 日付のみ
S1 時刻のみ
S2 日付と時刻
S3 日付と時刻(今日、昨日に限り文字で表示)

これらを組み合わせて指定すると希望する書式に変換できます。

変換結果 @Text の書式 補足
2024/01 D3S0 年月
2024/01/02 D0S0 年月日
03:04 T1S1 時分
2024/01/02 03:04:05 D0T0S2 年月日時分秒

@Text による変換の場合、必ずゼロ埋めになるようです。


@Text 仕様値の注意点

@Text を使用して日付を変換した場合、そのフォーマットは OS の日付の設定に依存します。例えば、日付(短い形式)を次の通り設定すると日付の区切り文字は ”-” となります。

次のように 月/日/年 の順にするとその順になってしまいます。

OS の設定に応じて結果も変わるところがポイントです。特に、グローバルな会社では注意が必要となりますね。


日付/時刻の変換

私はというとクライアント環境に依存して動作が変わることを避けたいので、@関数を使って日付値を文字列にする場合以下の式を使用しています。SourceDatetime が変換したい日付/時刻値が入ったフィールドです。

   xSrc := SourceDatetime;

   @If(xSrc = ""; @Return(""); @Success);

   REM {桁ごとに変換(不要な桁は削除)};
   xY := @Text(@Year(xSrc));
   xM := @Text(@Month(xSrc));
   xD := @Text(@Day(xSrc));
   xH := @Text(@Hour(xSrc));
   xN := @Right("0" + @Text(@Minute(xSrc)); 2);
   xS := @Right("0" + @Text(@Second(xSrc)); 2);


   REM {日付文字列を生成};
   xDate := @Implode(@Trim(xY:xM:xD); "/");
   xTime := @Implode(@Trim(xH:xN:xS); ":");

   @Implode(@Trim(xDate:xTime); " ");

赤字の部分がポイントです。年月日時分秒の各桁ごとに文字列で変換しています。0 埋めしたい場合は、分 や 秒のように @Right を使用しています。

もし、秒 が不要であれば xS の行を消すだけとなります。時刻が不要であれば、xH, xN, xS の行を消します。この操作だけで、後述の @Trim と @Implode がうまく調整してくれて、希望の文字列が出力されます。


2024/09/29

Notes - Excel 連携:#40)訂正 - 名前アイコン生成

前回まで、3 回にわたって名前アイコン作成について紹介しました。その中で一部プログラムに訂正が発生したのでアップデートさせていただきます。訂正箇所は『#38)名前アイコン生成 ②』の以下の部分です。

--- 以下、訂正箇所

   dPoint = PixcelToPoint(viPixcel-1)

ちなみに端数処理の関係かはわかりませんが 1 ピクセル大きく出力されたので、1 引いています。

--- ここまで


問題点

まず発生する症状です。

作成するアイコンのサイズが奇数の場合、幅が 1 Pixcel 小さくなる症状が発生しました。

試しに 10 Pixcel から順にサイズを大きくしながらアイコンを作成したところ、以下のようになりました。


原因

記載時点でうすうす気づいていた通り、端数処理の問題ですね。内部処理で端数が出ない場合、1 を引くとサイズが小さくなるということなのでしょう。

ただ、高さについては指定通りのサイズになっています。幅と高さで端数処理が違うということになります。Excel は難しいですね。


修正内容

サイズそのままだと、当初経験した通りサイズが大きくなることがあり、今回 1 を引くと引きすぎということがわかりました。そこで、次のように修正しました。

   dPoint = PixcelToPoint(viPixcel) - PixcelToPoint(1) / 2

Pixcel は整数なので、1 ピクセルをインチに変換してから、半分にして引いています。

この結果、指定したサイズで出力されるようになりました。もちろん、大きなサイズであっても正しく出力されています。

 


まとめ

今回は、過去の記事の訂正でした。検証の甘さで正しくない情報を公開してしまいました。記載時点で気になっていたのであれば、その時検証しなかったのか後悔しています。

実際の業務においても、業務の込み具合や費用対効果、時間の都合などで、黙殺することがあります。ただ、トラブルが発生しないことを黙殺するのはいいですが、問題が発生するのはよくありません。このあたりの嗅覚が腕の見せ所ですよね。もっと磨かねばと感じました...。


前回 DXL Step-by-Step


2024/09/27

DXL Step-by-Step:#39)セルの設定 - サンプルコード

DXL 活用の調査・検証で、実現できたことや発見したことご紹介する『DXL Step-by-Step』シリーズの第 39 回です。直近 2 回、『#37)セルの設定 - 背景色』と『#38)セルの設定 - 表の罫線』の設定を行うプログラムを作成します。


サンプルの内容

この時点で最新のサンプルコードである『#36)罫線の設定 - 境界線のスタイル』のプログラムをもとに作業します。

実現する機能は、#37 で行った背景のグラデーション設定と #38 の罫線の幅の設定です。


セルの取得

背景色や罫線の幅の設定はセルのノード tablecell の属性設定となります。そこで、表 table ノードからセルを取得する関数を作成します。引数は table ノードと取得する行と列となります。

Function xGetDXL_cell(vdenTbl As NotesDOMElementNode, ByVal viRow As Integer, ByVal viCol As Integer) As NotesDOMElementNode
   'セルのマージは考慮しない
   Dim denRow As NotesDOMElementNode

   '行を取得
   Set denRow = GetChildByName_Nth(vdenTbl, "tablerow", viRow)

   'セルを取得
   If Not(denRow Is Nothing) Then
      Set xGetDXL_cell = GetChildByName_Nth(denRow, "tablecell", viCol)
   End If
End Function

ライブラリ lsDXL 内に事前に作成した GetChildByName_Nth 関数を利用しています。この関数は、指定した名前の子ノードの中から n 番目のノードを返す関数でした。

まず、この関数で指定された行の tablerow ノードを取得しています。そして同様に tablerow ノード配下の tablecell ノードを取得して戻り値としています。

もし、引数の行や列が大きい場合などは Nothing を返します。


なお、この関数はセルのマージを考慮していません。あらかじめご了承ください。


背景色と罫線の幅の設定

上記関数を使用してセルが取得できたら、背景色と罫線の幅の設定を行います。DXL を作成する関数 xSetDXL を変更します。

今回作成した関数を使用して、真ん中のセルのノードを取得。背景色を設定したあと、四方の罫線の幅を 3 ピクセルに設定しています。

Function xSetDXL(vdprs As NotesDOMParser)
         ・・・
   Call xSetDXL_colwidth(denTbl, 3, "30%")

   '境界線のスタイル
   'Call denTbl.SetAttribute("cellborderstyle", "ridge")
   Call denTbl.SetAttribute("cellbordercolor", "green")

   'セルの取得
   Dim denCell As NotesDOMElementNode
   Set denCell = xGetDXL_cell(denTbl, 2, 2)


   '背景色の設定
   Call denCell.SetAttribute("bgcolor", "#e0e0ff")
   Call denCell.SetAttribute("altbgcolor", "#ffffd0")
   Call denCell.SetAttribute("colorstyle", "hgradient")


   '罫線の設定
   Call denCell.SetAttribute("borderwidth", "3px")

   '表後の段落追加
   Call xSetDXL_par(ddn, denRT)
End Function

ただ、これで完成ではありません。この状態で実行すると次のようになります。

#38 で紹介したように隣接するセルの幅も設定する必要があります。今回は、正しく表示されていない右と下のセルについて設定します。

         ・・・
   '罫線の設定
   Call denCell.SetAttribute("borderwidth", "3px")

   '右と下のセルの罫線の調整
   Set denCell = xGetDXL_cell(denTbl, 2, 3)
   Call denCell.SetAttribute("borderwidth", "1px 1px 1px 3px")
   Set denCell = xGetDXL_cell(denTbl, 3, 2)
   Call denCell.SetAttribute("borderwidth", "3px 1px 1px 1px")


   '表後の段落追加
   Call xSetDXL_par(ddn, denRT)
End Function

以上で完成です。


まとめ

今回はセルの操作についてサンプルコードを紹介しました。

罫線の設定においては、隣接するセルの設定を行うことがポイントとなります。今回のサンプルでは最低限必要な、右と下のセルだけ設定しましたが、理想を言えば上や左のセルも設定するほうが正しいと言えます。

設定位置が違うので当たり前なのですが、上下左右で幅を指定する文字列はそれぞれ違います。また、1 行目などセルの位置によっては隣接するセルがない場合があります。

これらすべてを考慮して確実に操作するコードを抜けもれなく確実に記述するのは大変です。その上、コードがだらだら長くなりますので見づらくなります。

実運用のコーディングでは関数化するなど工夫が必要ですね。


前回 DXL Step-by-Step 次回