2024/09/03

作ってみよう:#10)お小遣い帳 - プレイスタイプの日本語化

前回までの対応で、お小遣い帳としては使える状態になりました。今後は使いながら気になった点を少しずつ機能を改善して、より使いやすくしていきます。

最初の改善は、前回作成したばかりの機能です。計画性がないですね...。

プレイスタイプを指定して購入場所を検索する機能を作成しました。この機能について、一部の方から drugstore など内部的な文字列(英語)ではなく、日本語表記としてほしいというご意見をいただきました。今回は、少し予定を変更して、この対応を行います。


プレイスタイプマスタの作成

まずは、単純なマスタフォームを作成します。基本的な操作ですので、詳細な手順や設定は割愛します。ご了承ください。

フォーム名 a.PlaceType
別名 fPlaceType

項目名 フィールド名 種類 補足
並び順 SortNo 数値
プレイスタイプ PlaceType テキスト
名称 Name テキスト

[閉じる][編集][保存]などの最低限のアクションボタンもあわせて作成します。


続いて、マスタメンテ用ビューを作成し、上記 3 つのフィールドを表示する列を配置します。当たり前ですが、並び順でソートしておきます。

ビュー名 a.PlaceType
別名 vPlaceType

ビューには[閉じる]と [新規作成]のアクションボタンを作成します。


ここまで出来上がったら、ビューをプリビュー表示して、選択肢として必要なマスタデータを作成します。


選択用のビューの作成

先ほど作成したビューをコピペして、選択用のビューを作成します。

ビュー名 (vPicPlaceType)
別名 vPicPlaceType

選択画面(PickList)では不要となるので、「並び順」を非表示にし、「プレイスタイプ」を削除します。また、ビューのプロパティで列ヘッダと選択用余白を非表示にします。


ライブラリの修正

lsGoogleMAP_UI を開き、ビュー経由でプレイスタイプを選択する関数を追加します。

Public Function PicPlaceType() As String
   Dim ndc As NotesDocumentCollection
   Dim nd As NotesDocument
   Dim s As String

   s = "検索するプレイスタイプを選択してください。"

   '選択画面の表示
   Set ndc = xnuiw.PickListCollection(_
         PICKLIST_CUSTOM , False, xndb.Server, xndb.FilePath,_
         "vPicPlaceType", xndb.Title, s)

   If ndc.Count > 0 Then
      '戻り値のセット(名称|別名)
      Set nd = ndc.GetFirstDocument()
      s = nd.GetItemValue("Name")(0)
      s = s & "|" & nd.GetItemValue("PlaceType")(0)
      PicPlaceType = s
   End If
End Function

選択画面は、NotesUIWorkSpace クラスの PickListCollection メソッドを使用しています。2 つ目の引数を False に設定し複数選択できないようにしていますが、戻り値は NotesDocumentCollection で返されます。1 以上の場合、選択したことになるので最初の文書を取得して PlaceType フィールドの値を戻り値にセットしています。


メインルーチンの修正

プレイスタイプを選択する関数が変更となったので、メインルーチンを調整します。

エージェント NearBySearch_Google の Initialize を次の通り修正します。

Sub Initialize
         ・・・
   Set oLoc = New Location(nd.Sch_Latitude(0), nd.Sch_Longitude(0))
   Set oSch = New NearBySearch(oLoc)

   'プレイスタイプの選択
   Dim s As String
   s = PicPlaceType()
   If s = "" Then Exit Sub

   v = Split(s, "|")
   oSch.SearchType = v(1)
   Call oSch.Search()

   '検索結果の確認
   If oSch.Count = 0 Then
         ・・・
End Sub

なお、元の状態ではプレイスタイプの選択をキャンセルした場合でも、検索を実行しているバグがありました。今回はキャンセル(=戻り値が null)の場合、処理を終了するように修正しています。


動作確認

修正が終わったらテストします。Nomad から [近隣]ボタンをクリックします。以下のように選択肢が日本語で表示されれば成功です。


今回の対応では、PickList を利用しました。選択肢の一覧の実体はビューとなります。文字色や文字サイズや行間などの設定ができるので、利用者のニーズに合わせて調整できますね。

前回 作ってみよう 次回


2024/09/01

複数のフィールド値をセットでソート

前回 LotusScript を使ってソートする方法を紹介しました。ただ、実際にアプリを作っていると前回の関数だけではうまくいかない問題に直面することがあります。

例えば、ノーツでありがちな複数のフィールドを同じ要素数だけ値を保持、改行表示することで表っぽく見せる場合です。

今回はこのような複数のフィールド値をセットでソートする場合を考えてみましょう。


力技には限界がある

今回の例では、ソート値以外に 2 つのフィールドがあります。これら値もソート順に従い並び替える必要があります。

単純に考えると、前回の関数に引数を追加、値の入れ替えが発生したタイミングで追加した値も入れ替えれば、実現できます。

Function xSort(vvData As Variant) As Variant  '引数に行先と交通費を追加
   Dim i As Integer
   Dim j As Integer
   Dim v As Variant
   Dim vRtn As Variant

   '戻り値配列を準備
   vRtn = vvData

   '値をソート
   For j = 1 To Ubound(vRtn)
      For i = 0 To Ubound(vRtn) - j
         If vRtn(i) > vRtn(i+1) Then
            '値を入れ替え
            v = vRtn(i)
            vRtn(i) = vRtn(i+1)
            vRtn(i+1) = v

            'ここで行先と交通費も入れ替え

         End If
      Next
   Next

   'ソートした配列を返す
   xSort = vRtn
End Function

ただ、今回のようにフィールドが 3 つ程度であればいいですが、10 や 20 フィールドだったらどうでしょう? このような力技は非効率で限界があることがわかります。


配列を活用して回避

この問題は配列をうまく活用すると比較的簡単に解決できます。

まずはソート関数 xSort の改造です。赤字の部分が修正箇所です。基本的な構造はそのままですが、インデックス配列なるものが登場しており、関数の最初でソートしたいデータと同じだけの配列を用意して、添え字番号をセットしています。値の比較では、添え字の指定がカスケードされており、入れ替えは値ではなく、インデックス変数となっています。また、戻り値がインデックス配列となっていますね。

Function xSort(vvData As Variant) As Variant
   Dim i As Integer
   Dim j As Integer
   Dim v As Variant
   Dim aiIdx() As Integer

   'インデックス配列を準備
   Redim aiIdx(Ubound(vvData))
   For i = 0 To Ubound(vvData)
      aiIdx(i) = i
   Next


   'ソート開始
   'インデックス配列経由で値を参照し、インデックスを入れ替え

   For j = 1 To Ubound(vvData)
      For i = 0 To Ubound(vvData) - j
         If vvData(aiIdx(i)) > vvData(aiIdx(i+1)) Then
            'インデックスを入れ替え
            v = aiIdx(i)
            aiIdx(i) = aiIdx(i+1)
            aiIdx(i+1) = v
         End If
      Next
   Next

   'インデックス配列を返す
   xSort = aiIdx
End Function


これがどのような挙動になるのか順にみていきましょう。

まずはソート開始前の状態確認です。引数で渡される配列と準備したインデックス配列は次の通りとなります。

i vData(i) aiIdx(i)
0 2024/08/30 0
1 2024/06/13 1
2 2024/10/25 2
3 2024/09/19 3


続いてソートを開始します。

ポイントとなるのが、vvData(aiIdx(i)) の表現です。例えば、i = 1 の場合では、aiIdx(i) = 1 となり、vvData(aiIdx(i)) = vvData(1) = 2024/06/13 となります。

現時点では意味がなく、ムダに見えますが、もう少し我慢してください。

実際のソートの処理を順に確認しましょう。インデックス配列 aiIdx 、ソートしたい値である vvData をループ変数の進展に合わせて表にまとめています。

j i aiIdx() vvData() 操作
0 1 2 3 aiIdx(i) aiIdx(i+1)
1 0 0 1 2 3 2024/08/30 2024/06/13 入れ替え
1 1 0 2 3 2024/08/30 2024/10/25
2 1 0 2 3 2024/10/25 2024/09/19 入れ替え
2 0 1 0 3 2 2024/06/13 2024/08/30
1 1 0 3 2 2024/08/30 2024/09/19
3 0 1 0 3
2
2024/06/13 2024/08/30
結果 1
0
3
2
ソート完了

いかがですか?

今回の処理では、ソートしたい値 vvData の値は一切変化していません。インデックス変数を並べ替え、そこを経由してアクセスすることで、ソートを実現しています。


インデックス配列の活用

ソート完了後はこのインデックス配列を戻り値としていました。そのインデックス配列と実データを引数で渡し、並び替えた実データを戻り値で返す関数です。

Function xGetSortedVal(vvIdx As Variant, vvData As Variant) As Variant
   Dim i As Integer
   Dim v As Variant

   '戻り値配列の初期化
   v = vvData

   '戻り値生成
   For i = 0 To Ubound(v)
      v(i) = vvData(vvIdx(i))
   Next

   xGetSortedVal = v
End Function

ソートの比較で使用したインデックス配列を経由して取得する方法を使用して、並び替えられた値を取得しています。


メインルーチンでは、これら関数を利用して次のように記述します。

Sub Click(Source As Button)
   Dim nuiw As New NotesUIWorkspace
   Dim nuid As NotesUIDocument
   Dim nd As NotesDocument
   Dim v As Variant

   Set nuid = nuiw.CurrentDocument
   Set nd = nuid.Document

   'ソートされたインデックス配列を取得
   v = nd.Src_Date
   v = xSort(v)


   'ソートされた実データを取得
   nd.Sort_Date = xGetSortedVal(v, nd.Src_Date)  '日付
   nd.Sort_Text = xGetSortedVal(v, nd.Src_Text)  '行先
   nd.Sort_Cost = xGetSortedVal(v, nd.Src_Cost)  '交通費

End Sub


関数のテスト

前回と同様に、簡単なテストフォームを作成して、動作確認します。

実行すると、次のようにすべてのフィールドが日付でソートされます。


これで、日付、行先、交通費の各項目を日付順で並び替えることができました。この方法であれば、セットで操作するフィールド数が増えてもあまり気にならないですね。

2024/08/30

LotusScript で値のソートを作成

ノーツでアプリ開発を行っていると値をソートしたくなることがあります。@関数には @Sort という関数があるのですが、LotusScript には存在しないようです。そこで、今回は LotusScript でソートする関数を作成します。


アルゴリズムとソート

プログラミングを勉強しているとアルゴリズムという言葉に絶対に出会いますよね。簡単に言うと問題解決の手法となるのですが、ソート(並べ替え)という問題はアルゴリズムを語るうえで格好の題材になります。

バブルソートやクイックソート、ヒープソートなど、ソートにはさまざまなアルゴリズムが存在します。わかりやすいこと、平均的に速いことなどそれぞれに特徴があります。また、ランダムな値を並べ替える場合やランキングのようにソートされたデータに値が 1 つ増えるような場合など、入力データの状態による得手不得手があります。

ソートは身近な問題ですが、なかなか奥が深いと言えます。ネットで検索するとたくさんの情報が出てきます。興味のある方は一度調べてみてください。きっと、プログラミングスキルの向上につながると思いますよ。


今回のテーマとバブルソート

アルゴリズムについて語ることが今回のテーマではありません。実際に LotusScript でソートを実現する方法について紹介することが目的です。ですので、今回は処理がわかりやすいバブルソートを採用します。

バブルソートは隣り合った値を比較、大小関係が違う場合入れ替えを行います。これを繰り返しソートを実現します。例えば、5, 7, 3, 1 という 4 つの値をバブルソートする操作は次のようになります。

5 7 3 1 5 と 7 を比較
5 7 3 1 7 と 3 を比較、入れ替え
5 3 7 1 7 と 1 を比較、入れ替え(7 が確定)
5 3 1 7 5 と 3 を比較、入れ替え
3 5 1 7 5 と 1 を比較、入れ替え(5 が確定)
3 1 5 7 3 と 1 を比較、入れ替え
1 3 5 7 ソート完了(すべて確定)

最初は 4 つすべての値で比較を行うので、比較は 3 回発生します。これで、最大値の 7 が確定しますので、次の比較は確定していない 3 つの値で比較しています。これを繰り返し、すべての値が確定したらソート完了です。


LotusScript で実現

上表の流れで LotusScript でバブルソートを実現すると次のようになります。

Function xSort(vvData As Variant) As Variant
   Dim i As Integer
   Dim j As Integer
   Dim v As Variant
   Dim vRtn As Variant

   '戻り値配列を準備
   vRtn = vvData

   '値をソート
   For j = 1 To Ubound(vRtn)
      For i = 0 To Ubound(vRtn) - j
         If vRtn(i) > vRtn(i+1) Then
            '値を入れ替え
            v = vRtn(i)
            vRtn(i) = vRtn(i+1)
            vRtn(i+1) = v
         End If
      Next
   Next

   'ソートした配列を返す
   xSort = vRtn
End Function

隣り合った要素の比較するため、ループ変数を使って i と i+1 で表しています。

         If vRtn(i) > vRtn(i+1) Then

そして、確定した要素をループからはじくため外側のループの値を使って内側のループ数をコントロールしています。

      For i = 0 To Ubound(vRtn) - j


関数のテスト

動作確認のため、簡単なテストフォームを作成します。

フィールドはそれぞれ複数値を設定可能としています。ボタンには先ほどの xSort 関数を配置し、テストコードを Click イベントに記述します。

Sub Click(Source As Button)
   Dim nuiw As New NotesUIWorkspace
   Dim nuid As NotesUIDocument
   Dim nd As NotesDocument
   Dim v As Variant

   Set nuid = nuiw.CurrentDocument
   Set nd = nuid.Document

   nd.Sort = xSort(nd.Src)
End Sub

実行すると、次のようにソートできることが確認できます。

2024/08/28

連番のリスト値の生成

今回も開発中の dxl ナンプレ のハイスコア機能に関するお話です。

内部的には、ハイスコアを記録した、ユーザ名、クリア時間、クリア日次をリスト値で保持しています。各リスト値の 1 番目が 1 位、次が 2 位という風にノーツではありがちな値の持たせ方ですね。

これを、ランキングとして表示する場合には、やはり順位の表示が必要です。今回はこの順位をリスト値の件数だけ表示するコードを紹介します。

必要な順位数を取得するために、今回も Sec フィールドを利用しています。このフィールドに値がない場合、ハイスコアがない状態。3 個のリスト値の場合は 3 位まで存在する状態という前提です。


リスト値を利用した方法

まずは、このブログ開始当初に紹介した方法の応用です。以下の記事では、Excel の列番号である A ~ Z、AA ~ ZZ のリストを作成する方法について紹介しました。

Excel の列文字列を式で算出

その時の技を使用すると次のような式になります。

xLst := "0":"1":"2":"3":"4":"5":"6":"7":"8":"9";
xIdx := @Elements(Sec);
@If(xIdx = 0;
   "";
   @Subset(@Subset(xLst;-9) : (@Subset(xLst;-9)*+xLst); xIdx)
);

ポイントは @If の中身ですね。

まず、以下の式は、0 ~ 9 の 後ろから 9 要素なので、1 ~ 9 となります。

@Subset(xLst;-9)

その後ろ、以下の部分が 10 ~ 99 までを一括作成しています。

(@Subset(xLst;-9)*+xLst)

”*+” が順列演算子と呼ばれるもので、リスト値同士をすべての組み合わせで加算し、それぞれのリスト値を返す仕様でした。

この 2 つの結果を ":" 演算子でリスト値としてつなげ、1 ~ 99 のリスト値を作成。最後に @Subset を使用して必要な順位までのリスト値を切り出しています。


Excel の列番号は A スタートでしたが、今回は 1 から始まる数字になります。また、2桁になった場合、00 ではなく 10 となるので、@Subset(xLst;-9) を使ってうまく調整しています。


この方法では、リスト演算を習得していないとパッと見て何をしているのかわかりにくいですよね。また、ハイスコアで 99 位まで表示することはそうそうありませんので、無駄が多いと言えます。

そこで今回は方法、ループの関数である @For で作成してみます。


@For ループで実現した場合

@For 関数は Notes 6 から搭載された関数です。私はそれ以前から Notes で開発していたため、リスト演算の方をよく使います。

今回、あえて @For を使ってみましたが、つまづいた部分があったのでまとめておきます。


まず、ヘルプを見ながら、次のようなコードを作成してみました。

xIdx := @Elements(Sec);

@For(xNum := 1; xNum <= xIdx; xNum := xNum + 1;
   xLst := xLst : xNum
);
xLst

xNum がループ変数で 1 から必要な順位数まで順にループさせます。

xLst 変数が順位の値をリスト値としてためる変数で、1、2、3 … と格納する想定で作成しました。ところが、この式ではエラーが発生して動作しません。

どうやら、xLst の初期値が文字列で、以下の演算の初回実行でエラーが出ているようです。

   xLst := xLst : xNum


そこで、xLst に数値の初期値 1 を与えるようにしました。また、1 は必ずセットされるのでループは 2 からのスタートとし、最後にランキングがない場合は "" となるように調整しました。

これで、無事エラーもなく順位が表示されるようになりました。

xIdx := @Elements(Player);

xLst := 1;
@For(xNum := 2; xNum <= xIdx; xNum := xNum + 1;
   xLst := xLst : xNum
);
@If(xIdx = 0; ""; xLst)

久しぶりに @For を使ってみました。プログラムはリスト演算よりわかりやすい印象を受けましたが、変数の初期化が必要な点が少しクセがありますね。


なお、今回の事例では、リスト演算の結果は文字列リスト、@For ループの場合は数値リストとなっております。ご注意ください。


2024/08/25

秒数を 時:分:秒 に見やすく変換

先日、『DXL 拡張ライブラリをリリースします!』の記事の中で、ライブラリの活用事例として数字パズルゲームを紹介しました。”数独” と紹介したのですが、商標登録されているようなので、より一般的な ”ナンプレ” と訂正させていただきます。商標登録は国内だけなので海外では ”Sudoku” も一般的に使用されているようです。スマホアプリでは ”Sudoku” が多数あったのでつい使ってしまいました...


さて、9 月 19 日の DominoHub 2024 Osaka に向けて DXL 拡張ライブラリ dxlSuite for LotusScript と新生 ”dxl ナンプレ” の開発を急ピッチで進めております。本日はその作業の過程で作成したコードを紹介します。


紹介する事例

下図は、開発中の dxl ナンプレ でパズルの問題(ステージ)を選択する画面です。

パズルの右側にはハイスコア機能を作成しました。過去にクリアを時間順にランキング表示する機能なのですが、この時間、内部的には秒で管理しています。これをそのまま表示すると、318.2、374.7、1356.9 となり、わかりにくいですね。

今回はこの秒数を 時 分 秒 でわかりやすく変換するコードを紹介します。


変換の仕様は次の通りとします。

  • 秒 の表示は少数 1 桁
  • 時 分 は存在する場合のみ表示
  • 時 分 秒 の区切り文字は ”:”
  • 上の桁が存在する場合は、0 編集
  • 24 時間を越えても 日 に変換しない

例えば次のように変換します。

6.5 6.5
60.5 1:00.5 上の桁が存在する場合は、0 編集
3600.5 1:00:00.5
360000.5 100:00:00.5 24 時間を越えても 日 に変換しない


サンプルコード

今回の変換は@関数と LotusScript で作成しました。変換手順をそろえていますので、比較すると言語の違いが確認できます。

変換の処理は、秒数を 時 分 秒 のそれぞれの桁に分離してから文字列にしています。分と秒については上の桁の存在確認をして、0 編集するか決定しています。


@関数の場合

計算結果フィールドの式として作成したコードです。最初の Sec が変換する秒数を保持しているフィールドです。

xSec := Sec;
@If(xSec = ""; @Return(""); @Success);

REM "分と秒に分離";
xMin := @Integer(xSec / 60);
xSec := xSec - xMin * 60;

REM "分を時間と分に分離";
xHour := @Integer(xMin / 60);
xMin := xMin - xHour * 60;

REM "時分秒毎に文字列化";
xStrH := @If(xHour > 0; @Text(xHour) + ":"; "");
xStrM := @If(xMin > 0; @Text(xMin) + ":"; "");
xStrM := @If(xStrH = ""; xStrM; @If(xStrM = ""; "00:"; @Right("0" + xStrM; 3)));
xStrS := @Text(xSec; "F1");
xStrS := @If(xStrH+xStrM = ""; xStrS; @Right("00" + xStrS; 4));

xStrH + xStrM + xStrS


LotusScript の場合

LotusScript では関数化しています。引数に変換する秒数を与えると変換された値が戻り値で返される仕様としています。

Function xSec2Str(Byval vdSec As Double) As String
  Dim dSec As Double
  Dim lMin As Long
  Dim lHour As Long
  Dim sStrS As String
  Dim sStrM As String
  Dim sStrH As String

  '分と秒に分離
  lMin = Int(vdSec / 60)
  dSec = vdSec - lMin * 60

  '分を時間と分に分離
  lHour = Int(lMin / 60)
  lMin = lMin - (lHour * 60)

  '時分秒毎に文字列化
  If lHour > 0 Then sStrH = Cstr(lHour) & ":"
  If lMin > 0 Then sStrM = Cstr(lMin) & ":"
  If sStrH <> "" Then
    If sStrM = "" Then
      sStrM = "00:"
    Else
      sStrM = Right("0" & sStrM, 3)
    End If
  End If
  sStrS = Format(dSec, "0.0")
  If sStrH & sStrM <> "" Then
    sStrS = Right("0" & sStrS, 4)
  End If

  xSec2Str = sStrH & sStrM & sStrS
End Function

細かな点ではありますが、時 分 を管理する変数を Long 型にしてオバーフローさせないようにしています。

2024/08/19

憑いてる? 見えてはいけないものが見える !?

お盆休み返上で、『DXL 拡張ライブラリをリリースします!』で紹介した LotusScript のライブラリを一心不乱に開発していました。その時いくつか不思議な事象に見舞われました。連発して発生しましたので、冷や汗がでまくりました。お盆だからなのかはわかりませんが、きっと”何か”が降りてきたのだと思います...

今回はそんな摩訶不思議な体験のレポートです。


エージェントがハングアップ

エージェントにライブラリを組み込みテストを行っていました。ライブラリを少しアップデートした後、テストしたところ、エージェントがハングアップするようになりました。

Ctrl+Break を連打しても実行が止まりません。まるでゾンビ化したようです。

仕方がないので、[HCL Notes の診断データを収集して HCL Notes を終了する]を実行したところ、無事ゾンビは退治できました。nsd は最強ですね。でも、もう一度実行したら、また発生します。起動したてのクリーンな環境でもハングアップ。何度実行しても同じです。OS を再起動しても症状は変わりません。

Ctrl+Break 無効化するゾンビのようです。

突然発生したので狼狽しましたが、デバッガで確認したら原因がわかりました。虫(デバッガ)はゾンビに有効なアイテムのようです。デバッガは素晴らしいですね!


のっぺらぼうなデバッガ !?

次の不思議体験は、先ほどの救世主 ”デバッガ” が魂を抜かれた話です。

デバッガを有効にして、テストエージェントを実行します。すると、デバッガは起動するのですが、中身が空っぽです。オブジェクトやイベントなど何も表示されません。デバッガは動こうとしているのに、大いなる力に吸い取られているようです...

ただ、完全に憑依されたわけではようです。かろうじて[停止]や[デバッガの終了]は利用できるので助かりました...。

ちなみに、メッセージボックスを仕込んでも何も表示されません。どうやら、この現象が発生するとスクリプトは全く実行されていないようです。これだと調査しようがありません...。


見えてはいけないものが見える !?

続いては、ライブラリのバグ修正を行っている際に発生しました。霊感は強い方ではないのですが『見えてはいけないものが見える』んです。

エージェント内の関数ではなく、ライブラリ内にある同名の関数を指しています !?

ライブラリ開発においては、変数や関数のスコープを意識して開発しています。インターフェースではないスクリプトライブラリ内の関数は、外部からアクセスできないように Private 化しています。

ところが、見えているんです! しかもコンパイルエラーも出ません!!

しかも、この現象、先ほどののっぺらぼうと徒党を組んで出現しました。実行もデバッグもできないので、具体的な調査や切り分けができず、ただただ狼狽するだけでした...。


持ってたわけではないので一安心

立て続けにこんな現象が発生したので、何かの祟りかと思いましたが、お盆が明る頃には原因が見えてきました。すべて理由がある現象でした。”持ってる”わけではなかったので安心しました。

さて、それぞれの現象、原因はわかりますか?

順番に確認しましょう。


ハングアップの理由

現象を端的に発生させるサンプルコードを作成しました。これは完全なバグです。

Option Declare

Sub Initialize
   Call xSample()
End Sub

Function xSample()
   Dim i As Integer
   On Error GoTo Label_Err
   i = CInt("a")
Label_Exit:

Label_Err:
   Resume Label_Exit
End Function

デバッグモードで実行したときの動作は次の通りでした。

デバッグモードで動作を確認するとすぐわかりますね。

原因は Label_Exit: の後ろに Exit Function を忘れていることです。これが原因となってエラー処理で永久ループを起こしていました。 

このような状況になった場合、デバッグモードではない通常実行では Ctrl + Break でエージェントを強制終了ができないようです(停止できる場合もあるようです)。


ちなみに実際のプログラムでは、i = CInt("a") に相当する型の不一致エラーを発見し、これを修正してこの関数の調査を終わっていました。それが原因で、Exit Function の記述漏れの発見に手間取ってしまいました。

エラー処理や While ループなど、永久ループの可能性がある場合は、先に構造をを定義しておくべきですね。実際の処理を記述し始めるとついつい忘れてしまいます。


なお、この現象については技術情報が公開されています。

LotusScript のエージェントがループしてしまった際に Ctrl + Break で中断できません


のっぺらぼうはだれが召喚した?

まずは、再現プログラムです。この現象の再現にはメインにプログラムとは別にスクリプトライブラリが必要です。

◇ エージェント

Option Declare
Use "lsPsychics"

Sub Initialize
   Dim ns As New NotesSession
   MsgBox ns.CurrentAgent.Title
End Sub

◇ lsPsychics スクリプトライブラリ

Option Declare

Sub Initialize
   Call xInitMe()
End Sub

Function xInitMe()
   Dim i As Integer
   i = CInt("a")
End Function


メインルーチンは、自分自身の DB 名をメッセージボックスで表示するだけの単純なプログラムで、ここに間違いはありません。

のっぺらぼうを召喚しているのは、組み込んでいる lsPsychics スクリプトライブラリでした。このライブラリ、Initialize で関数 xInitMe を呼び出して初期化しています。ただ、この関数内にバグがありエラーが発生しています。

実は、デバッガの仕様でスクリプトライブラリの Initialize はデバッグされません。この現象は、以下の通り技術情報が公開されていました。

Script Library の Sub Initialize 内のコードがデバッグできません

ちなみに、ライブラリの Initialize 内に Stop を記述するとデバッグできるとのことです。


見えないものが見えたのはバグ

この現象が発生するサンプルコードです。Use で呼び出している lsPsychics スクリプトライブラリは前述のままとします。

Option Declare
Use "lsPsychics"

Sub Initialize
   Call xInitMe()
End Sub

Function xInitMe()
   'エージェントの初期化処理
End Function

この状態で Initialize の xInitMe 関数のポップアップヘルプを表示すると lsPsychics スクリプトライブラリを参照しているかのように表示されます。

実際に実行すると、正常にエージェント内のものが実行されます。また、エージェント内の xInitMe 関数を削除するとコンパイルエラーが出ますので、言語仕様的な問題はありません。単にデザイナーのポップアップヘルプのバグということになります。

この現象も技術情報が公開されていました。

Private の 関数やサブルーチンが他のスクリプトライブラリから参照できてしまう


やっぱりなにか ”持っている” ??

すべての現象はそれぞれ明確な原因がありました。ここにまとめたように、単純化した再現プログラムを作成でき、システム的な要因であることがわかりました。

ただ、それぞれの原因を見るとかなりレアな現象だと思います。それが同時多発的に絡み合って発生するなんて、何か ”持っている” のか何かに ”取り憑かれた” のかもしれませんね...

 

2024/08/17

DXL Step-by-Step:#38)セルの設定 - 表の罫線

DXL 活用の調査・検証で、実現できたことや発見したことご紹介する『DXL Step-by-Step』シリーズの第 38 回です。前回からは表の ”セル” 構造についてまとめていますが、今回は表の罫線の設定についてです。


罫線の設定と DXL

表の罫線を設定するプロパティにはいくつかあります。今回のテーマは 境界線の厚さ です。この設定は、セルごとに行えますので、tablecell ノードの属性で設定されます。

ちなみに、プロパティ上部の境界線のスタイルや色については表全体の設定になりますので、table ノードの属性設定となります(『#36)罫線の設定 - 境界線のスタイル』参照)。


境界線の厚さと DXL

まずは単純に幅1の表を作成して DXL で出力して確認します。

結果は、borderwidth という属性に出力されていました(属性は抜粋して記述)。

<tablecell borderwidth='1px'>

幅の単位は pixcel なので、1px となっています。罫線を非表示(= 0)とした場合は 0px となります。


四方の設定値を変えた場合

セルの罫線の幅は 4 辺それぞれ別の値が設定できます。例えば、幅を 0 ~ 3 とすべて違う値に設定た結果は次の通りです。


<tablecell borderwidth='0px 1px 2px 3px'>

上 右 下 左 の順でスペース区切りで指定されていることがわかります。


その他のパターン

さまざまな設定を DXL で出力していると borderwidth の値が 2 つ、あるいは 3 つの場合に出くわすことがあります。

例えば、上下は 1px、左右が 3px の場合です。


<tablecell borderwidth='1px 3px'>

上 右 下 左 の順で、値が繰り返す場合は省略できるのでしょうか? では、3 つ場合は? と手探りな調査を行うには限界があるので、ここはヘルプを頼ります。日本語で記述されていたころのヘルプに明確な記述がありました。

  • 長さが 1 つ指定された場合、この長さが 4 辺すべてに適用されます。
  • 長さが 2 つ指定された場合、1 番目の長さは上辺と下辺に、2 番目の長さは左辺と右辺に適用されます。
  • 長さが 3 つ指定された場合、1 番目の長さは上辺に、2 番目の長さは左辺と右辺に、3 番目の長さは下辺に適用されます。
  • 長さが 4 つ指定された場合、順に上辺、右辺、下辺、左辺に適用されます。

DXL を少しでも短くする工夫なのでしょうが、なかなか複雑な仕様ですね。1 つと 4 つの二通りで十分じゃないかと思います...


DXL で設定する場合の注意

上記の通り、罫線の幅はセルごとに四方の設定を保持できる仕様です。しかし、表は通常隣接するセルがあり、セル間の罫線は 2 つの罫線設定が存在します。

ノーツクライアントで 3 x 3 の表を作成し、中心の罫線の幅を変更してみました。表内の文字は、DXL から取得した borderwidth の値です。

ノーツクライアントの場合、隣接する罫線の幅も調整されていることがわかります。

DXL で同様の操作をする場合、隣接するセルを取得して設定して回る必要があります。セルの連結まで考慮するとなかなか複雑です。


ちなみに、セル間で設定がそろっていない場合の挙動について確認します。先ほどと同じ 3 x 3 の中心のセルだけ 3px、それ以外をすべて 1px で指定した DXL を作成し、Import してみました。

結果は次の通り、右と下の罫線は 1px となりました。優先順位は上から下、左から右となっているようです。

整合性があってない状態ですが、文書を再保存してもこの設定は維持されました。ノーツクライアントとして許容できる範囲のようです。隣接するセルの罫線を調整してくれる機能は、プロパティボックスが実施している機能のようですね。


前回 DXL Step-by-Step 前回


2024/08/16

DXL Step-by-Step:#37)セルの設定 - 背景色

DXL 活用の調査・検証で、実現できたことや発見したことご紹介する『DXL Step-by-Step』シリーズの第 37 回です。今回からは表の ”セル” 構造について調査した結果をまとめます。


表の構造とセル

まず、表の DXL 構造に関して再確認します。下図は表の DXL のサンプルです。

table ノードが表全体を表します。その配下には、まず、列を定義する tablecolumn ノードが存在します。その後に行を表す tablerow ノードが、行の数だけ繰り返されます。tablerow ノードの配下には、列の数だけ tablecell ノードがあり、その配下にセル内のコンテンツが配置されるという仕組みでした。


セルの背景色の設定

セルの最初のテーマは、プロパティボックスのこの部分、セルの色 の設定です。

背景色の設定は単に色を設定するだけでなく、スタイルの設定でグラデーションの設定ができます。そして、設定した場合にはグラデーションの色が表示され、もう一色設定できるようになります。


セルの背景色と DXL

DXL はこのセルの背景色に対応しています。これら設定は、すべて tablecell ノードの属性として反映されます。セル属性ですから、セルごとにバラバラの設定が可能ということですね。

属性 設定値 補足
bgcolor 'red' など
'#8242ff' など
背景色
altbgcolor グラデーション時に使用
colorstyle 'vgradient' 縦のグラデーション
'hgradient' 横のグラデーション

bgcolor で背景色を設定します。色の設定値については 『#22)文字の装飾』の「文字の色」と同様ですので詳細はそちらを確認してください。

グラデーションの設定を有効にする場合、colorstyle 属性を作成し、グラデーションの方向を指定します。グラデーションを指定した場合には、altbgcolor 属性でもう一色を指定します。グラデーションが縦の場合は下側、横の場合は右側の色となります。

なお、これら属性を作成しない場合、セルの背景色は透明となります。


DXL のサンプル

例えばセルの背景色を下記の色で横のグラデーションで設定した表のセルがあったとします。

これを DXL 変換すると、以下のようになります。

<tablecell bgcolor='#e0e0ff' altbgcolor='#ffffd0' colorstyle='hgradient'>


前回 DXL Step-by-Step 次回


2024/08/12

つないでみよう:#20)GPT4o は OCR? - レシート読み取り

前回は、GPT4o に画像認識させ、その結果を JSON で受け取ることができました。今回は、その JSON を分析して必要な値を取得できるようにします。


JSON を解析する準備

今回の作業ではレスポンスの JSON を操作することになります。それには NotesJSONNavigator のオブジェクトが必要となります。そこで、これまで使用していた xAskGPT 関数の戻り値を文字列から NotesJSONNavigator のオブジェクトに変更します。

Function xAskGPT(ByVal vsJSON_Post As String) As NotesJSONNavigator
         ・・・
   sURL = "https://api.openai.com/v1/chat/completions" 'エンドポイント
   Set jnav = http.Post(sURL, vsJSON_Post)

   'Responceをセット
   Set xAskGPT = jnav
End Function

この変更に伴い、メインルーチンも調整します。

Sub Initialize
         ・・・
   sJSON_Post = xGetJSON(nd)
   'Call xSetRT(nd, "JSON_Send", sJSON_Post)
'必要な時だけ有効化

   'GPT4o 問い合わせ
   Dim jnavResponce As NotesJSONNavigator
   Set jnavResponce
= xAskGPT(sJSON_Post)

   'レスポンスの JSON を取得
   sJSON_Responce = jnavResponce.Stringify
   Call xSetRT(nd, "JSON_Responce", sJSON_Responce)

   Call nd.Save(True, False)
End Sub

ここまでで機能的な変化はありませんが、この状態を今回のスタート地点とします。


汎用関数の準備

階層化された JSON から必要な値を取得するにはプログラムが長く複雑になりがちです。その対策として、子エレメントを操作する汎用的な関数を 2 つ作成します。


◇ xGetChildElementByName 関数

名前を指定して、子エレメントを取得する関数です。

Function xGetChildElementByName(voParent As Variant, ByVal vsName As String) As NotesJSONElement
   Dim jobj As NotesJSONObject
   Dim sType As String

   On Error Resume Next
   sType = TypeName(voParent)

   If sType = "NOTESJSONELEMENT" Then
      Set jobj = voParent.Value
      Set xGetChildElementByName = jobj.GetElementByName(vsName)
   ElseIf sType = "NOTESJSONNAVIGATOR" Then
      Set xGetChildElementByName = voParent.GetElementByName(vsName)
   ElseIf sType = "NOTESJSONOBJECT" Then
      Set xGetChildElementByName = voParent.GetElementByName(vsName)
   End If
End Function


◇ xGetChildElementByName 関数

名前を指定して、子エレメントの値を取得する関数です。

Function xGetChildValueByName(voParent As Variant, ByVal vsName As String) As String
   Dim je As NotesJSONElement
   Dim sType As String

   On Error Resume Next
   sType = TypeName(voParent)

   If sType = "NOTESJSONELEMENT" Then
      Set je = xGetChildElementByName(voParent, vsName)
   ElseIf sType = "NOTESJSONNAVIGATOR" Then
      Set je = voParent.GetElementByName(vsName)
   ElseIf sType = "NOTESJSONOBJECT" Then
      Set je = voParent.GetElementByName(vsName)
   End If

   xGetChildValueByName = je.Value
End Function


レスポンスの確認と作成する機能

前置きが長くなりましたが、ここからが本題です。

まずは、レスポンスの JSON を再確認します。整形した全体は次の通りとなります。

OCR としての JSON は choices - message - content の順でエレメントを取得すればよいということですね。また、content の値が JSON となっているカスケードされた構造となります。

今回作成する機能は、この content の値を NotesJSONNavigator のオブジェクトとして取得するところまでとします。オブジェクト化しておけば、レシートの項目に柔軟にアクセスできますね。


OCR としての JSON の取得

content の値をオブジェクトで取得する関数は以下の通りです。上記の JSON 構造に従い順に取得しています。

Function xGetJSONObj_OCR(vjnav As NotesJSONNavigator) As NotesJSONNavigator
   Dim je As NotesJSONElement
   Dim ja As NotesJSONArray
   Dim sOCR As String

   'choices 取得
   Set je = xGetChildElementByName(vjnav, "choices")
   If Not (je Is Nothing) Then
      'choices は配列なので NotesJSONArray で受ける
      Set ja = je.Value
      If ja.Size > 0 Then
         '配列の 1 件目を取得
         Set je = ja.GetFirstElement()
         'message 取得
         Set je = xGetChildElementByName(je, "message")
         If Not (je Is Nothing) Then
            'content 取得
            sOCR = xGetChildValueByName(je, "content")
            'オブジェクト化して戻り値にセット
            Set xGetJSONObj_OCR = xns.CreateJSONNavigator(sOCR)
         End If
      End If
   End If
End Function

準備段階で説明した関数を利用しているので、この関数は JSON の操作に特化できています。このように、JSON の煩雑な操作を分離しておくとプログラムの可読性が上がりますね。


メインルーチンの修正とテスト

最後にメインルーチンであるエージェントを修正して、作成した xGetJSONObj_OCR 関数をコールします。また、OCR としての JSON は NotesJSONNavigator  オブジェクトで返されます。これを使用して、JSON をテキストとしてフィールドに保存、汎用関数を経由して、合計金額を抽出して表示しています。

Sub Initialize
         ・・・
   Dim jnavOCR As NotesJSONNavigator
         ・・・
   'GPT4o 問い合わせ
   Set jnavResponce = xAskGPT(sJSON_Post)
   sJSON_Responce = jnavResponce.Stringify
   Call xSetRT(nd, "JSON_Responce", sJSON_Responce)

   'OCR としての返答取得
   Set jnavOCR = xGetJSONObj_OCR(jnavResponce)
   Call xSetRT(nd, "JSON_OCR", jnavOCR.Stringify())

   MsgBox "合計金額 = " & xGetChildValueByName(jnavOCR, "合計金額")


   Call nd.Save(True, False)
End Sub

なお、フォームには取得した JSON を確認するために JSON_OCR フィールドを追加してください。

実行するとこのフィールドに JSON が表示され、合計金額がメッセージボックスで表示されます。


まとめ

GPT4o の画像認識機能を使用して、OCR として利用することに挑戦しました。今回のプログラムで返される JSON は次の通りでした(整形済み)。

{
   "店舗名":"FamilyMart 南堀江店",
   "購入日":"2024/07/08",
   "合計金額":"278"
}

今回はサンプルですのでシンプルな構造としました。

指示の仕方によっては、購入品名や単価など明細を取得できます。明細は複数になるので、カンマなど区切り文字で返させたり、階層化した JSON で返させることも可能なようです。すべては、AI の役割の設定とリクエストの仕方次第ということですね。

ご興味のある方はいろいろと試してみてください。


前回 連載:つないでみよう 次回


2024/08/11

つないでみよう:#19)GPT4o は OCR? - 返答を JSON で返す方法

前回まで、GPT4o で画像認識させる方法をまとめました。画像を認識できるなら、挑戦したくなることがありますよね。そう、Optical Character Reader(OCR)です。

ということで、今回から GPT4o を OCR としての利用にチャレンジしたいと思います。

OCR の活用事例として思い浮かぶのは、レシートや領収証、名刺などの読み取りですね。システムでの利用を考えると、単にテキスト化するのではなく、項目と値を関連付けして再利用しやすい形で取得したいですね。

今回は、WebAPI 系の連載らしく OCR としての読み取り結果を JSON 形式で取得することを目指します。


前回作成したアプリでテスト

GPT4o はリクエストに JSON で返答するように伝えると答えてくれます。なかなか優秀ですね。前回作成したテストツールをそのままの状態で、次のようなリクエストを作成します。

AI の役割 あなたはOCRです。ユーザがリクエストした画像データから、必要な文字を認識し、ユーザに返します。返信するフォーマットはJSON形式とし、返信する項目は、ユーザがリクエスト時に指示します。
リクエスト この画像はレシートです。この画像から「店舗名」、「購入日」、「合計金額」を読み取ってください。   

そして適当なレシート画像を添付して GPT4o に送信します。

受信した JSON は次の通りで、返答である content ノードの値は次ようになっていました。


以下は、画像のレシートから読み取った情報です:\n\n```json\n{\n  \"店舗名\": \"FamilyMart 南堀江店\",\n  \"購入日\": \"2024年7月8日\",\n  \"合計金額\": \"¥278\"\n}\n```

改行などが含まれていて見づらいですが、項目名と値がセットになった JSON が確かに含まれています。ただ、セリフなど尾ひれがいっぱいついています。結果をシステム的に処理するためには邪魔になりますね。


JSON だけを返す方法

この問題は、GPT4o に問い合わせを送信する Request Body にオプションを設定することで解決できます。詳しくは下記のリファレンスを参照ください。

response_format


リファレンスの記載に合わせて『#16)GPT4o で画像認識 - 送信する JSON と作成 ①』で作成した xGetJSON 関数を修正します。

Function xGetJSON(vnd As NotesDocument) As String
         ・・・
   Set jo = joCnt.Appendobject("image_url")
   s = "data:image/jpeg;base64,{" & sBase64 & "}"
   Call jo.AppendElement(s, "url")


   'response_format
   Set jo = jnav.Appendobject("response_format")
   Call jo.AppendElement("json_object", "type")


   xGetJSON = jnav.Stringify

End Function

修正後、生成される JSON には次の通り response_format ノードが追加されます。

実行するとレスポンスが次のように変わりました。


{\n  \"店舗名\": \"FamilyMart 南堀江店\",\n  \"購入日\": \"2024年7月8日 7:40\",\n  \"合計金額\": \"¥278\"\n}


返答の精度向上

これで、GPT4o からの返答は JSON だけに限定できました。

ただ、日付値に時刻があったりなかったり、金額に¥が含まれるなど、処理しにくいデータとなっています。そこで、AI の役割でもう少し細かく設定し、返答を向上させます。

AI の役割 あなたはOCRです。ユーザがリクエストした画像データから、必要な文字を認識し、ユーザに返します。返信するフォーマットはJSON形式とし、返信する項目は、ユーザがリクエスト時に指示します。
日付項目はyyyy/mm/dd、金額など数値項目は、\マークやカンマなどの編集文字はつけないでください。

結果は次のように変化しました。

{\n  \"店舗名\": \"FamilyMart 南堀江店\",\n  \"購入日\": \"2024/07/08\",\n  \"合計金額\": 278\n}

次回は、この JSON を分析して、必要な値を取得する部分を作成します。


前回 連載:つないでみよう 次回