出直し!! ヘルプ

連載中

連載 終了

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 を分析して、必要な値を取得する部分を作成します。


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


2024/08/07

DXL 拡張ライブラリをリリースします!

来る 2024 年 9 月 19 日に DominoHub 2024 Osaka が開催されます。

DominoHub

このイベントで、DXL 拡張ライブラリのリリースを発表することとなりました!

ミニセッションで紹介、展示スペースでデモを計画しております。ご興味のある方はぜひともご参加ください。


今回は、そのライブラリのご紹介です。

と言いますのも現在もリリースに向け、必死になって開発&デバッグをしております。お盆休みにサボらないよう、自身にムチを打つための投稿となります...


ライブラリ作成の背景

LotusScript でリッチテキストを触っていて、できないことが多くフラストレーションを感じたことはありませんか? 私は、リッチテキストに画像を見える状態(インラインイメージ)で貼り付けられないことがきっかけでした。

その要望を HCL Domino Ideas Portal に投稿したところ、DXL でできると一蹴されたのです。それじゃあ、ってことで調べ始めたのが DXL との出逢いでした。ただ、DXL に関してはドキュメントが少なく、調査は難航しました。

ただ、調査が進むに従い、DXL の可能性を感じるようになりました。きっかけとなったインラインイメージの操作だけでなく、リッチテキストの知る限りのコンテンツを詳細な設定まで操作できるようでした。さらには、設計要素まで操作できることまでわかりました。

ただ、DXL(Domino XML Language)を理解して、適切に操作する必要があり決して ”簡単” ではなさそうです(詳細は連載『DXL Step-by-Step』を参照ください)。

せっかくの機能を活用したいということで、DXL を簡単に扱えるライブラリを作ろうということになりました。構想から約 2 年、人前に出しても恥ずかしくないレベルにはなったので、この度リリースするととしました。

名付けて『dxlSuite for LotusScript』です。

そう、古株の方はお気づきですよね。Lotus 時代、前衛的な製品で eSuite という製品があり、それにあやかって命名しました。DXL 自体は新しい機能ではないので、先鋭的で興味深いライブラリになればとの思いを込めております。


dxlSuite の特徴

ライブラリの特徴は次の 3 点です。

1. DXL で標準クラスの限界を突破!

ノーツクライアントでリッチテキストに配置/操作のできる多数のオブジェクトに対応。それぞれのオブジェクトの細かなプロパティにもアクセスできます。

2. クラスライブラリとして提供

ライブラリはクラスライブラリとして提供。タイプアヘッドによる入力補助など標準クラスと同様の使用感で利用いただけます。

3. シンプル&パワフル

チュートリアル & オンラインヘルプを装備しています。もちろん日本語です(笑)。その上、ライブラリ内に開発支援機能を搭載しています。複雑なリッチテキストの処理を標準クラスより簡単/効率的に開発ができます。


活用事例

dxlSuite の機能を使って作成したサンプルアプリを紹介します。

チマタによくある数独アプリですね。選択したセルや入力した数字と同じマスを色付け、メモ機能を搭載したノーツで遊べる数独です。

dxlSuite の機能を使えばこんなアプリを作ることができます。


サンプルアプリの構造

この数独アプリの構造を紐解きながら、dxlSuite でどのようなことができるか紹介します。

フォームの設計にはリッチテキストが 1 つあるだけです(Step 数など一部隠しフィールドは存在)。このリッチテキストフィールド内にゲーム板全体の画面を DXL で生成しています。

画面構成の大枠を表で作成し、各セルに画像をインラインイメージで貼り付けています(未選択は透明の画像)。マスの色は、表の背景色で表現しています。入力可能なマスは画像に対してホットスポットのアクションを設定しており、クリックすると事前に準備したスクリプトライブラリの関数をコール仕組みとなっています。

このように dxlSuite でノーツ標準の機能を組み合わせて実現しております。利用した機能をざっとまとめると次の通りです。

  • 表を作成し、罫線の色や幅を設定
  • 表の各列の幅を固定
  • 各マスに画像(イメージリソース)をインラインイメージで配置
  • 入力項目にはホットスポット(アクション)を設定し、各マスの座標を引数にした LotusScript のプログラムを設定
  • 選択マスを青、その影響範囲を水色、問題をグレーで表の背景色を設定


メモ機能で入力すると、もともとの入力欄の画像とホットスポットを削除して、3 x 3 のカスケード表を作成。各マスに画像を縮小して配置しています(以下の画面では、構造がわかるようカスケードした表の罫線を薄く表示しています)。

この部分で使用している dxlSuite の機能は、

  • セル内の既存コンテンツを削除して、カスケードの表を追加
  • 画像を縮小して配置

となります。


DXL は UI のコントロールができません。そこで、操作毎の画面を新しい文書で組み上げバックエンドで保存。その文書を UI にスイッチさせています。この操作は 標準クラスの NotesUIWorkspace や NotesUIDocument を活用して実現しています。

この仕様では、操作ごとに履歴のように文書が生成されますが、これらは Undo や Redo で利用しています。


リリースに関して

dxlSuite for LotusScript の正式な発表は DominoHub 2024 Osaka で行いますが、現時点の計画では、ネットショップサービスを経由した有償販売を予定しております。

このライブラリ、構想開始から約 2 年、最新バージョンだけでもかなりの工数をかけて製作してきました。これまで 30 年にわたる Notes/Domino アプリ開発経験で最大級のライブラリで、かなりのコーディング量となっています。

それを回収することが目的ではありません。いくら注意をしてても、必ず、改善点やバグなどが発生します。このような状況になった場合により確実にご案内できるよう、有償という形態を考えております。

また、必要とされた方が明確になります。もし、ニーズが多いようでしたら、設計要素の対応などの機能強化やより便利になるような改善など、バージョンアップを考えたいと思います。


最後に

dxlSuite for LotusScript の機能を紹介させていただきました。LotusScript の標準クラスでは実現できないことがいろいろと含まれていたことがお判りいただけたかと思います。このほかにも機能がありますし、どのようなコーディングになるのか、開発環境や開発支援機能など気になることもあるかと思います。

ご興味を持たれた方は、DominoHub 2024 Osaka にお越しください。体験コーナーで、見て、触って、実際に感じていただけます。

2024/08/03

ロケーション文書の操作

ノーツクライアントの管理をしていると連絡先(names.nsf)の各種設定(文書)の確認や設定変更が必要となることがあります。このような作業は得てしてトラブル対応の場合に多く、時間的余裕がないことが多々あります。そんな状況に備えて、手順を整理しておきます。

先日、ロケーション文書の設定の確認が必要になりました。今回は、その際に利用したプログラムをサンプル化して保管しておきます。


ロケーション文書の取得

まずはすべてのロケーションを順に取得する方法です。

連絡先である names.nsf の Locations ビューに接続。順に文書を取得するだけの単純なプログラムです。下記のサンプルでは文書内の Name フィールドからロケーション名を取得して表示しているだけです。実際には、この部分を必要な処理に変えるだけですね。

Option Declare

Sub Initialize
   Dim ns As New NotesSession
   Dim ndb As NotesDatabase
   Dim nvLoc As NotesView
   Dim ndLoc As NotesDocument
   Dim s As String

   Set ndb = ns.GetDatabase("","names.nsf")

   'ロケーション文書を順に調査
   Set nvLoc = ndb.GetView("Locations")
   Set ndLoc = nvLoc.GetFirstDocument
   While Not (ndLoc Is Nothing)
      '発見
      'ロケーションに対する処理をここに記述

      s = ndLoc.GetItemValue("Name")(0)
      MsgBox s

      Set ndLoc = nvLoc.GetNextDocument(ndLoc)
   Wend
End Sub


現在のロケーションを取得

次は、現在使用中のロケーションだけを取得する方法です。

現在使用中のロケーション名は notes.ini の Location というエントリーに格納されています。NotesSession クラスの GetEnvironmentString メソッドで取得しています。

その値と各ロケーション文書の名称を比較して、現在使用中のロケーションを判断しています。

Option Declare

Sub Initialize
   Dim ns As New NotesSession
   Dim ndb As NotesDatabase
   Dim nvLoc As NotesView
   Dim ndLoc As NotesDocument
   Dim sCur As String
   Dim s As Variant

   Set ndb = ns.GetDatabase("","names.nsf")

   'notes.ini から現在のロケーション名を取得
   sCur = ns.GetEnvironmentString("Location", True)
   sCur = StrLeft (sCur, ",")


   'ロケーション文書を順に調査
   Set nvLoc = ndb.GetView("Locations")
   Set ndLoc = nvLoc.GetFirstDocument
   While Not (ndLoc Is Nothing)
      s = ndLoc.GetItemValue("Name")(0)
      If s = sCur Then

         '発見
         'ロケーションに対する処理をここに記述

         MsgBox s
      End If

      Set ndLoc = nvLoc.GetNextDocument(ndLoc)
   Wend
End Sub