出直し!! ヘルプ

連載中

連載 終了

2023/10/31

リスト値の判定

アプリケーション開発をしていると値を比較することは頻繁にあるかと思います。単純に2つの値を比較するだけなら簡単なのですが、この値の片方、または双方がリスト値となった場合は少し複雑です。

そこで、リスト値の判定に関してまとめます。調査する比較方法は次の4種類です。主題は ① と ② の演算子なのですが、参考までに ③ と ④ の@関数も比較してみます。

A = B 並列演算による比較
A *= B 順列演算による比較
@IsMember(A; B) A が B に含まれるか判定
@Contains(B; A) A が B に含まれるか判定(部分一致含む)
引数の順に注意してください


テスト I

まずは、基本的なテストです。

B の値を固定して、調べる値である A を変化させた場合を確認します。A の値は、完全に一致と一致する要素が含まれるかどうかで変化させました。

@IsMember は A のすべてが B に含まれている必要があることがわかります。それ以外の比較では、一致するものが1つでもあれば True となっています。

A B A = B A *= B @IsMember
(A; B)
@Contains
(B; A)
1 abc
def
ghi
abc
def
ghi
True True True True
2 abc abc
def
ghi
True True True True
3 abc
xyz
abc
def
ghi
True True True
4 xyz abc
def
ghi

※ 空白の欄は False  


テスト II

テスト I と同様のテストですが、A と B の値を逆転させた場合です。

一致するものが1つでもあれば True であることが確認でき、テスト I と同様の結果です。

A B A = B A *= B @IsMember
(A; B)
@Contains
(B; A)
1 abc
def
ghi
abc True True True
2 abc
def
ghi
abc
xyz
True True True
3 abc
def
ghi
xyz


テスト III

リストにぬけがある場合です。

並列演算である A = B が True となるんですね。意外な結果ですので、この点は注意が必要です。

@IsMember は、すべての要素が一致する必要があるので、III-1 のテストは False となります。

A B A = B A *= B @IsMember
(A; B)
@Contains
(B; A)
1 abc
def
ghi
abc
ghi
True True True
2 abc
ghi
abc
def
ghi
True True True True


テスト IV

次はリスト値の順序の確認です。要素の出現順を入れ替えた場合です。順列演算の場合は、True と判定されています。

一致しない要素を入れてみましたが、特別な結果はありませんでしたね。

A B A = B A *= B @IsMember
(A; B)
@Contains
(B; A)
1 abc
def
ghi
ghi
abc
True True
2 abc
def
ghi
ghi
abc
xyz
True True
3 ghi
abc
abc
def
ghi
True True True
4 ghi
abc
xyz
abc
def
ghi
True True


テスト V

部分一致のテストです。@Contains 以外は部分一致では反応しませんでした。

A B A = B A *= B @IsMember
(A; B)
@Contains
(B; A)
1 ab abc
def
ghi

True
2 hi
xy
abc
def
ghi

True
3 xy abc
def
ghi


テスト VI

参考までに null の場合やリスト値でない場合の結果もまとめておきます。

調べる値 A が null の場合、@IsMember と @Contains で差がある点に注意が必要ですね。

A B A = B A *= B @IsMember
(A; B)
@Contains
(B; A)
1 (null) (null) True True True True
2 abc (null)
3 (null) abc True
4 abc abc True True True True
5 ab abc True
6 xy abc
7 abc ab
8 abc xy


まとめ

値の比較とリスト値の関係をまとめました。これらの挙動を理解した上で、バグを出さないよう注意して開発しましょう。

また、プログラム内で使用されている = 演算子がリスト値まで意識しているのか判断に困ることがたまにあります。特に別の開発者が作成したアプリの場合、特にそうですよね。こういった場合に備え、@IsMember などの関数を積極的に使用したほうがリストを意識したことが明示的となり、わかりやすいかもしれませんね。

2023/10/29

つないでみよう:#2)ChatGPT API の利用

Web 系アプリ開発のど素人が、チャレンジする WebAPI 連携の日記『オールドタイプでもできる(かも)WebAPI 連携』の第 2 回です。

前回に続き ChatGPT です。前回作成した API を使って API を実際に利用してみます。まずは、OpenAI が提供している API のリファレンスを確認します。

OpenAI API reference


OpenAI には様々な種類の API があるようですね。ブラウザの翻訳機能の力を借りつつ読み進めると、API 利用に必要な情報が集まります。今回は、Chat に関する API についてまとめます。


リクエストヘッダ

リファレンスの GETTING STARTED の Making Request に記述がありました。curl コマンドの記述ではありますが、リクエストヘッダにセットすべき項目が読み取れます。

$OPENAI_API_KEY には、前回取得したキーを設定するということですね。


エンドポイント

リファレンスの ENDPOINTS を読み進めます。どうやら Chat 機能を実現する API は chat completion と呼ぶようですね。Create chat completion セクションには、次の記載があります。

POST  https://api.openai.com/v1/chat/completions

ここから HTTP リクエストメソッドは POST であることと送信する URL がわかります。


リクエストボディ

API に送信するリクエストの中身を JSON で指定します。指定する JSON のフォーマットは、Create chat completion セクション直下の Request body に記載があります。必須項目は次の2つだけのようです。

messages は array とありますので、配列で指定するようです。プロパティを開いて詳細を確認すると、role と content が必須であることがわかります。


レスポンス

API からの返答については The chat completion object に記載があります。リクエストした発言に対する応答は choices の messeage にセットされます。ChatGPT ではまれに複数の返答を返すことがありますが、これに対応するために choices は配列になっているのだと思います。

usage オブジェクトには、今回のコールで使用されたトークン数が出力されます。トークン数は課金のベースとなります。


サンプルプログラム

これで必要な材料がそろったので、これらを使ってサンプルプログラムを作成します。

フォームを新規作成して、サンプルプログラムの動作を記録する RequestBody と Responce フィールドを配置します。ボタンを作成し、次のプログラムを記述します。

Sub Click(Source As Button)
   Dim ns As New NotesSession
   Dim nuiw As New NotesUIWorkspace
   Dim nuid As NotesUIDocument
   Dim nd As NotesDocument
   Dim sURL As String
   Dim http As NotesHTTPRequest
   Dim jnav As NotesJSONNavigator
   Dim ja As NotesJSONArray
   Dim jobj As NotesJSONObject

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

   'HTTP リクエストの準備
   Set http = ns.CreateHTTPRequest()

   'HTTP ヘッダーの設定
   Call http.SetHeaderField("Content-Type", "application/json")
   Call http.SetHeaderField("Authorization", "Bearer (ここにAPIキーをセット)")

   'RequestBody(JSON) の準備
   Set jnav = ns.CreateJSONNavigator("")
   Call jnav.AppendElement("gpt-3.5-turbo","model")
   Set ja = jnav.AppendArray("messages")
   Set jobj = ja.AppendObject()
   Call jobj.AppendElement("user", "role")
   Call jobj.AppendElement("1たす4は?", "content")
   'JSON をフィールドにセット(確認用)
   Call nd.ReplaceItemValue("RequestBody", jnav.Stringify)

   'API 実行
   http.PreferJSONNavigator = True
   sURL = "https://api.openai.com/v1/chat/completions"    'エンドポイント
   Set jnav = http.Post(sURL, jnav.Stringify)
   'Responce の JSON をフィールドにセット(確認用)
   Call nd.ReplaceItemValue("Responce", jnav.Stringify)
End Sub


実行と結果の確認

フォームをプリビューして、ボタンをクリックすると次のようになり、API に送信した Request body と API からの返答が JSON で表示されます。

まず、Request body です。整形すると次のようになります。

{
   "model": "gpt-3.5-turbo",
   "messages": [
      {
         "role": "user",
         "content": "1たす4は?"
      }
   ]
}

リファレンスに倣って model には、"gpt-3.5-turbo" を指定しました。API に渡す内容は、messages に配列で指定します。こちらからの発言なので、role は "user"とし、質問内容を content にセットしています。


Responce の JSON は次のようになっていました。以下の部分でこちらからの質問に答えていることがわかりますね。

{
   "id": "chatcmpl-8EOI9TaVJOocOQeA5Y16qQgh9MqhG",
   "object": "chat.completion",
   "created": 1698440661,
   "model": "gpt-3.5-turbo-0613",
   "choices": [
      {
         "index": 0,
         "message": {
            "role": "assistant",
            "content": "1たす4は5です。"
         },
         "finish_reason": "stop"
      }
   ],
   "usage": {
      "prompt_tokens": 13,
      "completion_tokens": 8,
      "total_tokens": 21
   }
}

API の role は "assistant" なんですね。


追加の質問

Request body の messages は配列で指定する仕様となっていました。これを利用するとこれまでの会話に対して追加の質問ができるようになります。

例えば、次のように API の返答と追加の質問を加えた JSON を作成して送信すると『5の2乗は25です。』と答えてくれます。

{
   "model": "gpt-3.5-turbo",
   "messages": [
      {
         "role": "user",
         "content": "1たす4は?"
      },
      {
         "role": "assistant",
         "content": "1たす4は5です。"
      },
      {
         "role": "user",
         "content": "それを2乗して!"
      }

   ]
}

このようにこれまでの会話を含めて次の質問をすることができますが、リクエストのたびに全体を送信する必要があります。そのため、リクエストが雪だるま式に大きくなり課金に跳ね返ってくるという結果となります。注意しましょう。


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

2023/10/28

つないでみよう:#1)新連載スタート!てはじめは今年はやった”アレ”

Web 系アプリ開発のど素人が、チャレンジする WebAPI 連携の日記です。自身の技術的な制約もありますが、Basic ノーツで実現できることを模索したことを思いつくままに記録する企画です。

簡単に表現すると『オールドタイプでもできる(かも)WebAPI 連携』です。

このような企画ですので、解説というよりはテスト結果を忘れないようレポートしておくような内容となります。予めご了承ください。


最初のネタは、今年はやった”アレ”です。

と言っても、59年ぶり関西対決が実現して盛り上がっている”トラ”の話ではなく、ChatGPT のことです。


準備作業

まずは OpenAI の API を利用する準備をします。

トップページ にアクセスして、ログインします。続いてのメニューで API をクリックします。

すると『Welcome to the OpenAI platform』メニューが表示されます。様々な情報があるのですが、まずは、API にアクセスをするため、アカウントメニューの[Manege account]を選択します。

左のメニューから[API keys]を選択し。[Create new secret key]をクリックして、API key を発行します。

発行されたキーが画面に表示されます。このタイミングしか表示されないので、文字列をコピーします。キーは何度も発行できるので、なにか問題があれば改めて発行してください。

このキーは API を利用する際に必要となります。API 利用者の特定に使用されますので、厳重に管理してください。


API 利用のコスト

OpenAI のAPI は、他の API と同様に無料枠が設定されています。ただ、OpenAI の場合、アカウントを作成したタイミングで付与され、API 利用にかかわらず一定期間で Expire するようです。

私の場合、ChatGPT がニュースなどで聞くようになったころ、一度ログインして試したことがありました。このタイミングで無料枠が付与されたのだと思いますが、2023年5月で期限切れとなっていました。

仕方がないので、有償プランを申し込み検証しました。有償プランは最低 5$ からとなっていて、有効期限は 1 年のようです。この期限、申し込み時点では 6 ヶ月と表示されていました。伸びる分にはありがたいですね。

現時点では、ChatGPT の 3.5 しか試していませんが、いろいろとテストしても $0.1 程度なので、開発やテスト用途では十分すぎる量があると思います。


おわりに

今回は OpenAI の API 利用の準備作業についてまとめました。次回は、API の利用に挑戦します。


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

2023/10/26

DXL Step-by-Step:#7)エージェント実行スケジュールの取得

前回は、エージェント(設計要素)を DXL に変換して内容を確認しました。その情報には、エージェントの実行スケジュールの設定のように LotusScript に標準搭載のクラスで取得できない情報も含まれており、エージェントが計画通り稼働しているか確認できるのではないかという、新たな可能性についてふれました。

今回は、これを題材に、エージェントの実行スケジュールを実際に取得してみます。


実行スケジュールの DXL の確認

まず、実行スケジュールを表す DXL を確認します。関係のあるエレメントと属性だけ抽出すると次のような構造になっていました。実行スケジュールを表すのは、赤字の部分です。

<?xml version="1.0" encoding="utf-16"?>
<!DOCTYPE agent SYSTEM "xmlschemas/domino_12_0_2.dtd">
<agent name='DXL2 #5-x.Sample' alias='SampleAgent' enabled='true'>
    <trigger type='scheduled'>
        <schedule type='byminutes' hours='3' minutes='0' onweekends='false'>
            <starttime>
                <datetime dst='false'>T090000,00</datetime>
            </starttime>
            <endtime>
                <datetime dst='false'>T180000,00</datetime>
            </endtime>
        </schedule>
    </trigger>
</agent>

今回は話を単純化するため、赤字以外の設定である週末の実行や有効時間帯の設定については無視させていただきます。


エージェントエレメントの取得

まずは、DXL からエージェント情報の全体を表す agent エレメントを取得します。ここまでのコードは、設計要素であっても、文書であっても全く同じです。

Function xShowAgentSchedule(vdprs As NotesDOMParser) As Boolean
    'DOM ツリーのルートを取得
    Dim ddn As NotesDOMDocumentNode
    Set ddn = vdprs.Document

    'Agent エレメントの取得
    Dim denAgent As NotesDOMElementNode
    Set denAgent = ddn.DocumentElement

        ・・・

End Function

NotesDOMElementNode クラスの DocumentElement プロパティは、エレメントのタグ名が "document" でなく、"agent" であっても取得できるんですね。


実行スケジュールの取得

次のコードは、実行スケジュールを表す "trigger" と "schedule" エレメントを順に取得し、"schedule" エレメントの属性から実行スケジュールを取得しています。

Function xShowAgentSchedule(vdprs As NotesDOMParser) As Boolean

        ・・・
    'トリガーとスケジュールの取得
    Dim denTrigger As NotesDOMElementNode
    Dim denSchedule As NotesDOMElementNode
    Dim dnl As NotesDOMNodeList
    Dim s As String

    Set dnl = denAgent.GetElementsByTagName("trigger")
    If dnl.NumberOfEntries > 0 Then
        Set denTrigger = dnl.GetItem(1)
        Set dnl = denAgent.GetElementsByTagName("schedule")
        If dnl.NumberOfEntries > 0 Then
            Set denSchedule = dnl.GetItem(1)
            s = "type = " & denSchedule.GetAttribute("type")
            s = s & ", hours = " & denSchedule.GetAttribute("hours")
            s = s & ", minutes = " & denSchedule.GetAttribute("minutes")

            MsgBox s
        End If
    End If
End Function

各エレメントの取得は、GetElementsByTagName で配下のタグを検索して取得しています。このメソッドは、NotesDOMNodeList を返す仕様となっていて、その中の一つ目を取得することで、目的のエレメントを取得しています。

この仕様は、複数のタグにも対応できるよう汎用的に設計されてはいるのですが、今回の用途ではコードが冗長になりますね。


このプログラムを実行すると、次のように実行スケジュールが表示されます。


DXL 開発の難しさ

今回はエージェントの実行スケジュールを取得方法をまとめました。サンプルでは、『1日1回以上』のエージェントを例にしました。別の設定の場合は、次のようになります。

属性 type の値が変わり、必要に応じてその他の属性が増減します。このあたりの構造の変化に柔軟に対応するプログラムの作成は手間がかかりそうです。また、条件に応じてどのような DXL が出力されるのかは、DTD を読み解くか、テストして DXL を確認する必要があります。

LotusScript の定義済みのクラスを使ったプログラムよりもずいぶんドロ臭いコーディングが必要となますね...


◇ 毎日

<schedule type='daily' onweekends='false'>

◇ 毎週

<schedule type='weekly' dayofweek='wednesday' onweekends='true'>

◇ 毎月

<schedule type='monthly' onweekends='true' dateinmonth='1'>


前回 DXL Step-by-Step 次回

2023/10/25

DXL Step-by-Step:#6)エージェント(設計要素)の DXL

前回からの続きです。文書として取得したエージェント(設計要素)を DXL に変換して確認します。前回掲載したプログラムの中で、赤字の部分が今回の主題となる部分です。

Sub Initialize
         ・・・
   Call nnc.BuildCollection()

   Dim dprs As NotesDOMParser
   If nnc.Count > 0 Then
      Set ndAgent = ndb.GetDocumentByID(nnc.GetFirstNoteID)
      Set dprs = xGetDOMParser(ndAgent)

      Call xSaveDXL(dprs)    'DXL の文字列を文書に保存
   End If
End Sub

まず、エージェント(設計要素)の文書を NotesDOMParser に変換します。この xGetDOMParser 関数は、第 3 回 で作成した関数を流用しています。

次の xSaveDXL 関数は、NotesDOMParser から DXL を文字列で抽出し、文書に保存させる関数です。コードは次の通りです。

Function xSaveDXL(vdprs As NotesDOMParser) As Boolean
   Dim ndb As NotesDatabase
   Dim nd As NotesDocument
   Dim nrti As NotesRichTextItem

   Set ndb = xns.CurrentDatabase
   Set nd = ndb.CreateDocument()
   nd.Form = "DXL"

   Set nrti = nd.CreateRichTextItem("Body")
   Call nrti.AppendText(xGetDXL(vdprs))

   xSaveDXL = nd.Save(True, False)
End Function

Function xGetDXL(vdprs As NotesDOMParser) As String
   Dim nst As NotesStream

   Set nst = xns.CreateStream()
   Call vdprs.SetOutput(nst)
   Call vdprs.Serialize()

   xGetDXL = nst.ReadText()
End Function

今後も DXL を確認する際に頻繁に利用することにりそうなので、重要な関数といえますね。


エージェントの DXL

さて、前置きが長くなりましたが、実行して結果を確認します。出力された DXL を VSCode で整形して確認します(整形については、『DXL ことはじめ:#3)DXL を見やすく整形』を参照)。

設計要素の中身が洗いざらい出力されています。エージェントのプログラムの中身や実行ログまでもが含まれています。


ポイントは次の赤枠の部分です。エージェントの実行スケジュールの詳細が出力されています。


ちなみに、LotusScript に標準搭載のクラス群の中に NotesAgent クラスがあり、Agent の設定などにアクセスできます。しかし、実行の設定に関しては、Trigger というプロパティしかなく、スケジュールエージェントであるかまでしかわからず、実行間隔までは取得できません。


DXL を使えば、実行スケジュールの詳細まで取得できます。この情報とエージェントの実行ログを比較することで、スケジュールエージェントが計画通り稼働しているか確認することも可能となります。

このように、DXL を応用すれば、LotusScript に標準搭載のクラス群を補完することができます。コーディングは少し(かなり?)面倒なのですが、これまであきらめていたことが実現できそうな可能性を感じますね。


前回 DXL Step-by-Step 次回

2023/10/24

DXL Step-by-Step:#5)設計要素の取得

これまではノーツの文書を DXL で操作する方法について説明してきましたが、今回は設計要素のアクセス方法について紹介します。


設計要素の確認

まず、ノーツ DB 内の設計要素の状態について確認します。

デザイナーで設計要素のプロパティを表示すると、設計要素も文書のようにフィールドを持っていていることが確認できます。例えば次の図は、エージェントのプロパティですが、$TITLE というフィールド名にエージェントの名称が記録されていることがわかります。

別名が "|" で連結されおり、いかにもノーツらしいですね...


NotesNoteCollection クラス と利用の流れ

LotusScript に標準搭載のクラス群の中に NotesNoteCollection というクラスが存在します。DXL を利用するまで、このクラスは使用したことがありませんでしたが、このクラスで設計要素にアクセスすることができます。

NotesNoteCollection (LotusScript®)


まずこのクラスには、SelectAgentSelectImageResources など、Select で始まるプロパティが多数存在します。このプロパティで取得する設計要素を限定できます。

また、SelectionFormula プロパティで、取得する設計要素を検索することができます。このプロパティと $TITLE フィールドを利用して、必要な設計要素を決定することになります。

取得条件が決定したら、BuildCollection メソッドを実行します。条件に一致したコレクションがこのオブジェクトにセットされ、そこから設計要素を取得するという流れとなります。


エージェントの取得方法

それでは実際にエージェントを取得してみます。

まずはメインルーチンです。赤字の部分がエージェントを検索して取得(コレクションの作成)をしている部分となります。SelectAgent プロパティと検索条件(後述)をセット、BuildCollection でコレクションを作成しています。

Option Declare
Private xns As NotesSession

Sub Initialize
   Dim ndb As NotesDatabase
   Dim ndAgent As NotesDocument

   Set xns = New NotesSession
   Set ndb = xns.CurrentDatabase

   Dim nnc As NotesNoteCollection
   Set nnc = ndb.CreateNoteCollection(False)
   nnc.SelectAgents = True
   nnc.SelectionFormula = xGetFormula("SampleAgent")
   Call nnc.BuildCollection()

          ・・・
End Sub

なお、検索条件は以下の xGetFormula 関数で作成しています。

Function xGetFormula(ByVal vsName As String) As String
   Dim s As String

   s = {xTitleList := @Explode($TITLE; "|");}
   s = s & {@IsMember("} & vsName & {"; xTitleList)}

   xGetFormula = s
End Function

この関数の戻り値は、引数の設計要素名を検索する式を返します。"SampleAgent" を指定した場合、次の文字列が返されます(見やすくなるよう改行を挿入)。

   xTitleList := @Explode($TITLE; "|");
   @IsMember("SampleAgent"; xTitleList)

この式では、$TITLEE フィールドの値を別名の区切り文字 "|" でリスト値に分離、その要素に引数の値が含まれていたらTrue を返します。

これで、引数の名前が設定されたエージェントだけが抽出できます。


BuildCollection を実行するとコレクションが作成されます。その後のプログラムでコレクション内の最初の文書を取得することで設計要素を文書として取得します。

Sub Initialize
         ・・・
   Call nnc.BuildCollection()

   Dim dprs As NotesDOMParser
   If nnc.Count > 0 Then
      Set ndAgent = ndb.GetDocumentByID(nnc.GetFirstNoteID)
      Set dprs = xGetDOMParser(ndAgent)

      Call xSaveDXL(dprs)    'DXL の文字列を文書に保存
   End If
End Sub


取得した設計要素 ndAgent をデバッガで中身を確認すると、次のように文書と同様に表示され、フィールドの中身が確認できます。内部的には、設計要素も文書として保存されていることがよくわかりますね。

次回は、この取得したエージェント(設計要素)の文書を DXL に変換して確認します。


前回 DXL Step-by-Step 次回

2023/10/22

@関数と LotusScript の Trim の差

Notes でアプリケーション開発をしていて使用する命令に Trim があります。文字列内のスペースを取り除いてくれる関数で頻繁に使いますよね。

先日、開発作業中に LotusScript の Trim 関数と @Trim で挙動が違うことに気が付きました。テスト結果とともに仕様を整理します。


テスト方法

次のようなフォームを作成しテストを実行しました。

各フィールドは次の設定としています。

フィールド名 種類
SrcStr テキスト / 編集可能
Func テキスト / 計算結果 @Trim(SrcStr)
LS テキスト / 作成時の計算結果 ""

更新ボタンのコードは下記の通りです。このボタンで @式を実行し、LotusScript の実行結果をフィールドにセットしています。

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

   Set nuid = nuiw.CurrentDocument
   Set nd = nuid.Document
   nd.LS = Trim(nd.SrcStr(0))

   Call nuid.Refresh()
End Sub


テスト結果

下表のとおりテスト文字列を変えながら結果を確認しました。

テスト文字列 @関数 LotusScript 備考
" "
(半角SPx1)
""
(null)
""
(null)
" "
(全角SPx1)
""
(null)
""
(null)
"    "
(半角SPx3)
""
(null)
""
(null)
"   "
(全角SPx3)
""
(null)
""
(null)
"    あ "
(前後の半角SP)
"あ" "あ" 個数に関係なく削除
"   あ "
(前後の全角SP)
"あ" "あ" 個数に関係なく削除
"あ い   う"
(間の半角SP)
"あ い う" "あ い   う"
(変化なし)
@Trimでは複数の半角スペースは1つに変換
"あ い   う"
(間の全角SP)
"あ い う" "あ い   う"
(変化なし)
@Trimでは複数の全角スペースは半角1つに変換
全角1つの場合はそのまま


まとめ

実験結果をまとめると次の通りです。

◇ 同じ結果となる処理

    • スペースだけで構成される文字列は、全角/半角を問わず null となる
    • 文字列の前後のスペースは、全角/半角を問わず消去される


◇ 挙動が違う処理( = 文字間のスペースの処理)

    • @Trim では、複数の全角スペースは半角スペース1つに変換
      (全角スペース1つの場合は全角のまま)
    • LotusScript では、文字間のスペースは処理されない

2023/10/20

Notes - Excel 連携:#24)式の設定

今回はセルに計算式を設定する方法についてまとめます。


式の設定

セルに式を設定する方法は、Range オブジェクトの Formula プロパティを使用します。

Range.Formula プロパティ (Excel)

次の図のように E10 のセルに E6 ~ E9 までの合計を計算させる場合を考えます。

LotusScript のコードとしては、次のようになります。なお、 oSheet にはこれまでの流れで WorkSheet オブジェクトがセットされている前提です。

   oSheet.Cells(10, 5).Formula = "=SUM(E6:E9)"

単一のセルのアクセスは Range オブジェクトを使用するより、Cells プロパティを使用して行と列を数値で指定(RC 形式)できるので便利です。ただ、設定する式では、列を文字であらわす形式(正式名称を知らないので”A1形式”と呼びます)の方がなじみがあります。


行列を A1 形式に変換

ループや加算減算などを考えるとプログラム内では数値で行列で指定する方が断然向いています。そこで、行と列を A1 形式の文字列に変換する関数を作成します。

まず、数値を文字列に変換する関数です。この関数では、1 を A、26 を Z、27 を AA に変換します(2桁までと単純化しています)。

Function x9ToA(ByVal viColNumber As Integer) As String
   Dim i As Integer
   Dim i1 As Integer
   Dim i2 As Integer
   Dim s As String

   i = viColNumber - 1
   i1 = (i Mod 26) + 1
   i2 = Int(i / 26)

   If i2 > 0 Then s = Chr(64 + i2)
   s = s & Chr(64 + i1)

   x9ToA = s
End Function

Mod は割り算の余りを求める演算子です。これを利用して、i1 変数には1桁目の数値を求めています。i2 は2桁目で Int を使って商の整数部分を求めて算出しています。

これに 64 を足して文字に変換し A - Z の文字列を取得しています(A の文字コードは 65)。


続いて、行と列を A1 形式に変換する関数を作成します。

Function xRCToA1(ByVal viRow As Integer, ByVal viCol As Integer) As String
   xRCToA1 = x9ToA(viCol) & CStr(viRow)
End Function


行単位の計算式

作成した関数を利用して、行単位の計算式を設定します。白黒とカラーの金額(枚数×単価)と合計金額の3か所を計算します。

ヘッダ行の次の行から、明細の行数(viDoc)分だけ順に式を設定します。

Function xCalcRow(voSheet As Variant, ByVal viDoc As Integer)
   Dim iRow As Integer
   Dim i As Integer
   Dim oRange As Variant
   
   iRow = xciHeaderRows
   For i = 1 To viDoc
      iRow = iRow + 1

      '白黒
      Set oRange = voSheet.Cells(iRow, 5)
      oRange.Formula = "=" & xRCToA1(iRow, 3) & "*" & xRCToA1(iRow, 4)
      'カラー
      Set oRange = voSheet.Cells(iRow, 8)
      oRange.Formula = "=" & xRCToA1(iRow, 6) & "*" & xRCToA1(iRow, 7)
      '合計
      Set oRange = voSheet.Cells(iRow, 9)
      oRange.Formula = _
                 "=Sum(" & xRCToA1(iRow, 5) & "," & xRCToA1(iRow, 8) & ")"
   Next
End Function


列単位の計算式

続いて、列単位の計算を行います。

次の関数は、指定した列(viCol)の合計を最終の明細行(xciHeaderRows + viDoc)の次の行に SUM 関数をセットします。

Function xCalcSum(voSheet As Variant, ByVal viCol As Integer, ByVal viDoc As Integer)
   Dim oRange As Variant
   Dim iMin As Integer
   Dim iMax As Integer

   iMin = xciHeaderRows + 1
   iMax = xciHeaderRows + viDoc

   Set oRange = voSheet.Cells(iMax + 1, viCol)
   oRange.Formula = _
              "=Sum(" & xRCToA1(iMin, viCol) & ":" & xRCToA1(iMax, viCol) & ")"
End Function


列単位の合計は、枚数と金額の計 5 列にセットします。

Function xCalcCol(voSheet As Variant, ByVal viDoc As Integer)
   Call xCalcSum(voSheet, 3, viDoc)
   Call xCalcSum(voSheet, 5, viDoc)
   Call xCalcSum(voSheet, 6, viDoc)
   Call xCalcSum(voSheet, 8, viDoc)
   Call xCalcSum(voSheet, 9, viDoc)
End Function


メインルーチンの修正

今回作成した関数を実行するためメインルーチンに以下の行を追加します。

Sub Initialize
        ・・・
   '使用量出力(年月度指定)
   iDoc = xPrintUsages(oSheet, 2023, 10)

   '計算式のセット
   Call xCalcRow(oSheet, iDoc)
   Call xCalcCol(oSheet, iDoc)


   'Excel を UI に表示
   oXls.Visible = True
End Sub

ここまでのプログラムを実行すると、下図のように合計が表示されます。


まとめ

今回は、セルに式を設定する方法について記載しました。シートにアクセスしようとすると、セルの指定や式の表現で A1 形式が必要となることが多いです。また、RC 形式の場合は”行・列”の順で、A1 形式の場合は”列・行”の順となります。セルの指定方法がバラバラで混乱しますね...

今回作成した関数を利用することで、”行・列”の順で固定化でき、あらぬバグを回避にもつながるかと思います。


前回 Notes - Excel 連携 次回

2023/10/17

Notes - Excel 連携:#23)明細データの出力

第 20 回からスタートした『帳票の作成』シリーズの 4 回目です。

今回は帳票の明細行となるノーツのデータを Excel に出力する部分を作成します。今回は、Notes - Excel 連携としては目新しい操作はありません。予めご了承ください。

なお、ノーツデータを Excel シートに出力する方法については、第 4 回 でくわしく紹介しております。必要に応じてご確認ください。


出力データの取得方法

今回の帳票では、指定した年月度の使用量データのみを出力対象とします。

そこで、対象データを検索するためのビュー vXlsSchUsage を作成します。青字のカラムはソートを設定しています。

このビューに対して検索して、対象文書を取得することとなります。

ただ、NotesView クラスの GetAllDocumentsByKey メソッドのデザイナーヘルプには、次のような記述があります(日本語で記載されていた 8.5 より抜粋)。

   このメソッドで返される文書は特定の順序で表示されることはありません。

ちなみに 12.0.2 のヘルプでも引き続き記載されていました。

   Documents returned by this method are in no particular order, ...

この記述は、『検索結果の NotesDocumentCollection オブジェクト内の文書の並び順がビューの順序と同じである保証はない』と理解しました(詳細に検証したわけではありませんので、解釈の間違いかもしれません)。

そこで、今回は、GetDocumentByKey を使用して最初の文書を取得し、ビュー経由で次の文書を順に取得する方法で実現します。この方法であれば、ビューのソート順に確実に従うことができます。また、出力順を変更したい場合にはビューの調整だけで済むので便利ですね。

Function xPrintUsages(voSheet As Variant, _
                       ByVal viYear As Integer, ByVal viMonth As Integer) As Integer
   Dim ns As New NotesSession
   Dim ndb As NotesDatabase
   Dim nv As NotesView
   Dim nd As NotesDocument
   Dim asKey(1) As Integer
   Dim iDoc As Integer '出力した行数

   Set ndb = ns.CurrentDatabase
   Set nv = ndb.GetView("vXlsSchUsage")
   nv.AutoUpdate = False

   asKey(0) = viYear
   asKey(1) = viMonth

   Set nd = nv.GetDocumentByKey(asKey, True)
   While Not (nd Is Nothing)
      iDoc = iDoc + 1
      Call xPrintUsage(voSheet, iDoc, nd) '明細出力

      '次の文書
      Set nd = nv.GetNextDocument(nd)
      If Not (nd Is Nothing) Then
         '年月が変わった場合ループ終了
         If Not(nd.Year(0) = viYear And nd.Month(0) = viMonth) Then
            Set nd = Nothing
         End If
      End If
   Wend

   xPrintUsages = iDoc
End Function

このループ処理では、ループの継続判定で2つのポイントがあります。

1点目は、次の文書の年月度が変わる場合の対応です。次の文書取得後、年月度を確認し、一致しない場合は Nothing をセットしてループを停止させています。

2点目は、次の文書が Nothing となる可能性があることです。ループで処理した文書がビューの最後の文書だった場合は、nd は Nothing となります。この状態で1点目の判定を行うとエラーとなります。そこで、事前に文書が Nothing であるかを判定しています。

なお、この関数の戻り値は、出力した行数としています。


明細データの出力

先ほどのリストでコールしている xPrintUsage が明細データを1行分出力する関数です。その関数のコードは次の通りとなります。

Function xPrintUsage(voSheet As Variant, _
                                ByVal viDoc As Integer, vnd As NotesDocument)
   Dim iRow As Integer
   iRow = viDoc + xciHeaderRows 'ヘッダエリア行数を加算

   '使用量出力
   voSheet.Cells(iRow, 2).Value = vnd.Device(0)    'デバイス名
   voSheet.Cells(iRow, 3).Value = vnd.BnW_Count(0)  '白黒
   voSheet.Cells(iRow, 4).Value = vnd.BnW_Unit(0)
   voSheet.Cells(iRow, 6).Value = vnd.Col_Count(0)    'カラー
   voSheet.Cells(iRow, 7).Value = vnd.Col_Unit(0)
End Function

引数の viDoc は、何行目の明細データかを表す数値です。これにヘッダの行数(後述)を加算して、Excel シート内の行番号を決定しています。


メインプログラム

作成した明細データを出力する関数をメインプロブラムからコールします。出力した明細の行数は後続の処理で使用しますので、変数 iDoc で受けて保持しています。

また、ヘッダエリアの行数を定数として登録しています。

Option Declare
Private Const xciHeaderRows = 5 'ヘッダエリア行数

Sub Initialize
   Dim oXls As Variant
   Dim oSheet As Variant
   Dim iDoc As Integer

   Set oXls = CreateObject("Excel.Application")
   Call oXls.Workbooks.Add
   Set oSheet = oXls.Workbooks(1).WorkSheets(1)

   '帳票の初期化
   Call xInit(oSheet)

   '使用量出力(年月度指定)
    iDoc = xPrintUsages(oSheet, 2023, 10)


   'Excel を UI に表示
   oXls.Visible = True
End Sub

ここまでのプログラムを実行すると、次のようになります。少しだけ帳票ぽくなってきましたね。

次回は、合計を Excel で計算させるため、セルに計算式をセットする方法について紹介します。


前回 Notes - Excel 連携 次回

2023/10/15

プロフィール文書の取り扱い

よく使う機能ですが、いざ使うとなるとついついヘルプを見てしまう機能にプロフィール文書があります。そこで、プロフィール文書の基本的な取り扱いについてまとめておきます。


プロフィール文書とは

nsf 内に保存できる特殊な文書です。文書ではあるのですが、DB のプロパティの文書数にカウントされず、ビューに表示することができないのが特徴です。そして、作成されたプロフィール文書は、アプリケーションの利用者で共有します。

このような特性から、一般的には、アプリケーションの設定などの保存に利用します。設計と文書の中間的な位置づけと言えますね。


プロフィール文書の作成方法

一般的な作成方法について記述します。

まず、プロフィール文書のフォームを作成します。

作成手順は通常のフォームと同様です。ポイントは、文書の作成方法です。次の式でフォームを開き、保存するとプロフィール文書として保存される仕組みとなっています。"fConfigForm" は、プロフィール文書のフォーム名です。

   @Command([EditProfile]; "fConfigForm")

保存後、もう一度このコマンドを実行すると、前回保存した内容が開き、内容を再編集し更新することができます。


LotusScript の場合、NotesUIWorkspace クラスの EditProfile メソッドを使用します。

   Call notesUIWorkspace.EditProfile("fConfigForm")


プロフィール文書のアクセス(@関数)

次の式でプロフィール文書から値を読み込みます。次の式では、指定したプロフィール文書の "DBPath" というフィールドの値を取得します。

   @GetProfileField("fConfigForm"; "DBPath")

次の式でプロフィール文書の "DBPath" というフィールドに値をセットします。

   @SetProfileField("Profile"; "DBPath"; "names.nsf")


LotusScript の場合

LotusScript からもプロフィール文書にアクセスできます。

ただ、@関数と違い直接的にプロフィール文書内のフィールドにアクセスするのではなく、NotesDocument のオブジェクト経由でアクセスします。プロフィール文書の取得は NotesDatabase クラスの GetProfileDocument メソッドを使用します。

   Dim ns As New NotesSession
   Dim ndb As NotesDatabase
   Dim nd As NotesDocument

   Set ndb = ns.CurrentDatabase
   Set nd = ndb.GetProfileDocument("Profile")

   MsgBox nd.DBPath(0)

そして、フィールドのアクセスは、NotesDocument オブジェクト経由で通常のフィールドと同様にアクセスできます。


もちろん、フィールドに値をセットすることができます。

   nd.DBPath = "names.nsf"

ただし、プロフィール文書の場合 Save メソッドを使用して明示的に保存する必要はありません。フィールドに値をセットするだけで、プロフィール文書が更新されます。

2023/10/13

リッチテキスト:#13)表のスタイル設定

これまで数回にわたってリッチテキスト内に表を作成し操作する方法について紹介しました。今回は表の操作のまとめとして、表のスタイル設定について整理します。


表のスタイルと色の設定

NotesRichTextTable クラスの Style プロパティを使うと表のスタイルを簡単に設定できます。

Style (NotesRichTextTable - LotusScript®)

このプロパティに設定できる値と設定されるスタイルは次の通りです。使いそうなものだけを抜粋しました。

0 TABLESTYLE_NONE 色をセットしない
1 TABLESTYLE_LEFTTOP 1行目と1列目に色をセット(Color)
2 TABLESTYLE_TOP 1行目に色をセット(Color)
3 TABLESTYLE_LEFT 1列目に色をセット(Color)
4 TABLESTYLE_ALTERNATINGCOLS 縦縞(AlternateColor, Color)
8 TABLESTYLE_SOLID 塗りつぶし(Color)

カッコ内に記載の Color と AlternateColor が使用する色です。この2つの色は、SetColorSetAlternateColor メソッドで設定します。色は NotesColorObject クラスを使用して設定します。


サンプルプログラム

前回のプログラムにスタイルを設定する部分を追加してみました(表の行列数は変更)。SetColor、SetAlternateColor をセットした上で Style を指定しています。

   '表の背景色
   Dim nco As NotesColorObject
   Set nco = ns.CreateColorObject()
   Call nco.SetRGB(240, 240, 255)
   Call nrttbl.SetColor(nco)

   Call nco.SetRGB(255, 255, 255)
   Call nrttbl.SetAlternateColor(nco)

    nrttbl.Style = TABLESTYLE_ALTERNATINGCOLS

実行すると次のように横縞の表となります。

参考までに Style を TABLESTYLE_LEFTTOP に設定すると以下のように変化します。

作成された文書を編集すると、表のプロパティの[表の色]が設定が確認できます。Style はこのプロパティをセットする機能であることがわかりますね。


表の操作まとめ

LotusScript でリッチテキスト内にアクセスするためには、NotesRichTextNavigator を使って走査する必要がありました。また、その影響か NotesRichTextTable を取得するには少し回りくどい方法となっていました。

また、今回紹介した表のスタイルの設定以外のスタイル変更はできなさそうでした。罫線の色や幅の変更、列幅の柔軟な設定や行列の間隔、セルの色などもう少し細かく設定できると自由度が増すと思います。

今後の機能拡張に期待したいですね...



前回 リッチテキストの基本操作