2023/12/12

DXL や JSON の日付値の変換

連載:DXL Step-by-Step では DXL(Domino XML Language)、連載:つないでみよう では JSON が頻繁に登場します。DXL(XML)も JSON もデータ構造を表す言語で、それぞれ標準化されていますが、日付/時刻値については共通の仕様になっています。

以下の画像は DXL と JSON 内の日付/時刻値の例です。


DXL 20230913T110634,71+09
気象庁 JSON 2023-12-01T11:00:00+09:00

この表記は ISO 8601 として標準化されています。この仕様について整理しつつ、変換プログラムを LotusScript で作成してみましょう。


ISO 8601 の仕様

ISO 8601 は、日付と時刻、タイムゾーンを文字列で表す仕様です。基本的には、日付を年 4 桁、月と日を 2 桁で表し、時刻の時分秒をそれぞれ 2 桁で表します。日時を表す各要素は、大から小で固定されており、月日年などの順序は認められていません。

表記方法として、基本形式と拡張形式が定義されています。基本形式は各要素を区切り文字なしで、拡張形式が区切り文字ありの書式と覚えればよいと思います。上記事例では、DXL が基本形式、JSON が拡張形式となります。


◇ 日付値

日付値は年月日の順で、年 4 桁、月と日を 2 桁で表します。

基本形式では 19970713 と 8 桁の連続した数値で表します。拡張形式は 1999-07-13 となり、区切り文字に "-" を使用します。"/" など他の区切り文字は認められていません。

なお、仕様としては、日付を省略し年月を表現したり、その年の何日目というような特殊な表現も定義されています。


◇ 時刻値

時刻値は、時分秒をそれぞれ 2 桁で表します。基本形式では T060328、拡張形式は T06:03:28 のようになります。区切り文字は ":" を使用します。

時刻の前には必ず "T" の文字が付加されます。また、時刻は 24 時制のみサポートしており、12 時制の定義はありません。

時刻値は、少数で表現できる仕様となっています(拡張形式の場合は最後の桁のみ)。T060328.50 のようにミリ秒を表すことができます。また、T06:03.12 で秒以下を少数表記したり、T06.123 で分以下を少数表記できるなど特殊な表記も定義されています。

なお、小数点は日本で使用されている "."(ピリオド)以外に ","(カンマ)も利用できます。上記 DXL の例ではカンマになっていますね。


◇ タイムゾーン

時刻の後ろに "Z" を指定すると、協定世界時(UTC)を表します。UTC より進んでいる時間帯の場合は "+"、遅れている場合は "-" を付加し、時差を 2 桁、もしくは 4 桁の数値で記述します。

上位2桁が時間を表します。4 桁の指定は一部地域で採用されている分単位の時差で必要となります。拡張形式での時間と分の区切り文字は ":" を使用します。


サンプルプログラム

ISO 8601 の日付/時刻値(文字列)をノーツで使用できる日付/時刻値(Variant 型)に変換する関数を作成してみました。

機能的には次の通りです。

  • 日付のみ、時刻のみ、日付/時刻値に対応
  • 標準形式と拡張形式に対応
  • 時刻はノーツのタイムゾーンに合わせて時差を換算

また、機能的に省略した点は次の通りです。

  • 日付の省略や時刻の少数表記などの特殊な指定
  • ミリ秒の計算(Variant 型の日付/時刻値で取り扱えないため)
  • 分単位の時差(NotesDateTime の TimeZone プロパティがサポートしていないため)

引数に ISO 8601 の日付/時刻値(文字列)を指定し、戻り値が日付/時刻値(Variant 型)の関数となっています。

Public Function ISO8601ToDateTime(ByVal vsString As String) As Variant
   Dim vTmp As Variant
   Dim vReturn As Variant

   If InStr(vsString, "T") = 0 Then
      '日付のみ
      vReturn = xISO8601ToDate(vsString)
   Else
      '時刻を含む
      If Left(vsString, 1) = "T" Then
         '時刻のみ
         vReturn = xISO8601ToTime(Replace(vsString, "T", ""))

         'オバーフロー対応
         If vReturn < 0 Then
            '時差計算で前日(負の値)になった場合、24時間を加算
            vReturn = vReturn + 1
         ElseIf vReturn >= 1 Then
            '時差計算で翌日になった場合、24時間を減算
            vReturn = vReturn - 1
         End If
      Else
         '日付/時刻値
         '時刻部/日付部を準備

         vTmp = Split(vsString, "T")
         vReturn = xISO8601ToDate(vTmp(0))
         vReturn = vReturn + xISO8601ToTime(vTmp(1))
      End If
   End If

   ISO8601ToDateTime = vReturn
End Function

ISO 8601 の文字列が時刻のみ指定されている場合、時差の計算を行った結果、前日になったり、翌日になったりオーバーフローする場合の調整を行っています。時刻のみを扱う事例に出くわしたことがないため、このインプリが正しいのかは判断できていません。予めご了承ください。


日付部、時刻部の変換はそれぞれサブ関数化しています。まず、日付値に変換する関数は次の通りです。

Function xISO8601ToDate(ByVal vsDate As String) As Variant
   Dim sTmp As String
   Dim iY As Integer
   Dim iM As Integer
   Dim iD As Integer

   '区切り文字を消去
   sTmp = Replace(vsDate, "-", "")

   '日付値に変換
   iY = CInt(Left(sTmp,4))
   iM = CInt(Mid(sTmp, 5, 2))
   iD = CInt(Right(sTmp, 2))
   xISO8601ToDate = DateNumber(iY, iM, iD)
End Function

続いて、時刻部の変換関数です。

Function xISO8601ToTime(ByVal vsTime As String) As Variant
   Dim sTime As String
   Dim vTmp As Variant
   Dim iH As Integer
   Dim iM As Integer
   Dim iSec As Integer

   Dim iTZ As Integer
   Dim bTZ As Boolean

   bTZ = True
   If InStr(vsTime, "Z") > 0 Then
      'UTC
      iTZ = 0
      sTime = Replace(vsTime, "Z", "")
   ElseIf InStr(vsTime, "+") > 0 Then
      'UTC より進んでいる
      vTmp = Split(vsTime, "+")
      iTZ = CInt(Left(vTmp(1), 2))
      sTime = vTmp(0)
   ElseIf InStr(vsTime, "-") > 0 Then
      'UTC より遅れている
      vTmp = Split(vsTime, "-")
      iTZ = - CInt(Left(vTmp(1), 2))
      sTime = vTmp(0)
   Else
      'タイムゾーン指定なし
      sTime = vsTime
      bTZ = False
   End If

   '時差計算
   If bTZ = True Then
      Dim ndt As New NotesDateTime(Now)
      iTZ = -(iTZ + ndt.TimeZone)
   End If

   '少数を削除
   vTmp = Split(sTime, ",")
   sTime = vTmp(0)
   vTmp = Split(sTime, ".")
   sTime = vTmp(0)

   '区切り文字を削除
   sTime = Replace(sTime, ":", "")

   '時刻値に変換
   iH = CInt(Left(sTime,2)) + iTZ
   iM = CInt(Mid(sTime, 3, 2))
   iSec = CInt(Right(sTime, 2))
   xISO8601ToTime = TimeNumber(iH, iM, iSec)
End Function

なお、日本の時差は UTC より 9 時間早いですが、LotusSript の TimeZone プロパティは -9、 ISO 8601 では +09 となり、± が逆転している点に注意が必要です。


2023/12/10

Windows のテンポラリフォルダの取得

LotusScript でプログラムを作成していると PC 内にファイルを一時的に保存したくなることがあります。ノーツ的にはデータディレクトリ内に利用しやすいのですが、私はずぼらなので、ついつい削除し忘れて他のファイルと混ざり収拾がつかなくなりがちです。

こうならないよう一時的なファイルは、Windows のテンポラリフォルダに保存するべきだと思います。そこで、LotusScript からそのフォルダを取得する方法を紹介します。

さまざまな方法があるのですが、今回はいにしえの呪文(?)である Windows API を利用して実現します。


利用する API

Kernel32.dll の GetTempPathA という関数を利用します。Microsoft Learn によると次の順序で確認し、見つかった最初のパスを返します。詳細は、GetTempPathA 関数 (fileapi.h) を参照ください。

  1. TMP 環境変数で指定されたパス
  2. TEMP 環境変数で指定されたパス
  3. USERPROFILE 環境変数で指定されたパス
  4. Windows ディレクトリ


サンプルプログラム

テンポラリフォルダの取得はさまざまなシーンでの利用が想定できるので、スクリプトライブラリ lsWindows を新規作成します。将来、Windows に関係する関数を作成することがあれば、ここに追加したいと思います。

Option Declare
Declare Function xGetTempPath Lib "kernel32.dll" Alias "GetTempPathA" (ByVal nBufferLength As Long, ByVal lpBuffer As String) As Long

Public Function GetWinTmpPath() As String
   Dim sPath As String
   Dim lStrLen As Long

   sPath = Space(255)
   lStrLen = xGetTempPath(255, sPath)

   GetWinTmpPath = Left(sPath, lStrLen)
End Function


作成した関数は、GetWinTmpPath() 関数で、戻り値は、テンポラリフォルダのパスです。結果をメッセージボックスで表示すると、次の通り最後に "\" が付加されています。


2023/12/08

つないでみよう:#4)天気予報取得のサンプルコード

Web 系アプリ開発のど素人がチャレンジする WebAPI 連携日記の第 4 回です。

前回に引き続き、気象庁の天気予報に関してです。前回は、Web サイトの仕組みやデータの取得方法、構造について整理しました。今回はその情報を使用して、ノーツで天気予報を取得するサンプルを紹介します。


サンプルプログラム

指定したエリアの天気予報を取得し、メッセージボックスで表示するだけの単純なエージェントです。メインルーチンは次の通りです。

Option Declare
Private xns As NotesSession

Sub Initialize
   Dim sAreaCD As String
   Dim vWeather As Variant

   Set xns = New NotesSession

   '大阪の天気予報を取得
   sAreaCD = "270000"
   Call xGetForecast(sAreaCD, vWeather)


   MsgBox Join(vWeather, Chr(10)), 64, "大阪の天気予報"
End Sub

エリアコードを指定して天気予報を取得する関数をコールします。戻り値の vWeather は文字列型の配列となる前提で、それをメッセージボックスで改行区切りで表示します。


天気予報の取得

天気予報を取得する関数は次の通りです。

引数で指定したエリアコードを使用してコールする URL を生成してから HTTP リクエストを実行しています。今回の API では、メソッドは Get となります。また、レスポンスは JSON を前提としています。

Function xGetForecast(ByVal vsAreaCD As String, rvWeather As Variant)
   Dim sURL As String
   Dim http As NotesHTTPRequest
   Dim jnav As NotesJSONNavigator
   Dim je3 As NotesJSONElement

   '天気予報の JSON を取得
   sURL = "https://www.jma.go.jp/bosai/forecast/data/forecast/" & vsAreaCD & ".json"
   Set http = xns.CreateHttpRequest()
   http.PreferJSONNavigator = True

   Set jnav = http.Get(sURL)

   '明後日までの天気予報の取得
   Set je3 = jnav.GetFirstElement()
   Call xGetForecast_3Days(je3, rvWeather)

End Function

返される JSON には、2つのオブジェクトがあり、明後日までの天気予報と週間天気予報となっていました。今回は、明後日までの天気予報を取得するので、1つ目のオブジェクト(エレメント)を取得しています。

実際に天気予報を取得するプログラムは、サブ関数 xGetForecast_3Days で行います。


明後日までの天気予報の取得

この部分は JSON の構造が少し複雑なので、取得したい天気予報(weathers)までの構造を改めて掲載します。

取得までの流れとしては、まず timeSeries を取得して、次に areas を取得、その中に目的の weathers が存在します。それぞれが配列の構造となっているので、段階を踏んで取得する必要があります。

具体的なプログラムは下記の通りですが、各段階の処理は理解しやすいようにコードをそろえるようにしました。流れとしては、

  1. エレメントを名称(キー)で検索し、NotesJSONElement オブジェクトとして取得
  2. 配列であるか確認
  3. 1つ目の要素を取得し、NotesJSONObject オブジェクトとして取得

としています。

Function xGetForecast_3Days(vje3Days As NotesJSONElement, rvWeather As Variant)
   Dim jeTmp As NotesJSONElement
   Dim jobjTmp As NotesJSONObject
   Dim jaTmp As NotesJSONArray

   Set jobjTmp = vje3Days.Value
   Set jeTmp = jobjTmp.GetElementByName("timeSeries")
   If jeTmp.Type = Jsonelem_type_array Then

      '配列の1つ目が天気予報
      Set jaTmp = jeTmp.Value
      Set jeTmp = jaTmp.GetFirstElement()
      Set jobjTmp = jeTmp.Value


      'エリア(areas)を取得
      Set jeTmp = jobjTmp.GetElementByName("areas")
      If jeTmp.Type = Jsonelem_type_array Then

         '配列だが1つしか要素がない
         Set jaTmp = jeTmp.Value
         Set jeTmp = jaTmp.GetFirstElement()
         Set jobjTmp = jeTmp.Value


         '天気
         Set jeTmp = jobjTmp.GetElementByName("weathers")
         rvWeather = xGetArray_String(jeTmp)
      End If
   End If
End Function

目的のオブジェクト weathers を取得した後は、その中身を配列に変換する関数を xGetArray_String をコールしています。

Function xGetArray_String(vje As NotesJSONElement) As Variant
   Dim jaVal As NotesJSONArray
   Dim jeVal As NotesJSONElement
   Dim i As Integer
   Dim vTmp As Variant

   If vje.Type = Jsonelem_type_array Then
      Set jaVal = vje.Value
      ReDim vTmp(jaVal.Size-1)
      For i = 1 To jaVal.Size
         Set jeVal = jaVal.Getnthelement(i)
         vTmp(i-1) = CStr(jeVal.Value)
      Next
   End If

   xGetArray_String = vTmp
End Function


まとめ

実行すると次のように、取得した天気予報を表示します。

サンプルがシンプルになるよう、天気予報をテキストで取得しているだけですが、このサンプルを応用すれば、降水確率や週間天気予報なども取得できるようになりますね。


今回は、気象庁の Web サイトから天気予報を取得するサンプルを紹介しました。

プログラム的には、

  1. HTTP リクエストを送信
  2. レスポンスが JSON となっている
  3. JSON を解析し、データを利用する

と、一般的な WebAPI と同じでしたね。このような背景から『つないでみよう』の連載で取り上げました。


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

2023/12/06

つないでみよう:#3)気象庁の天気予報

Web 系アプリ開発のど素人が、チャレンジする WebAPI 連携日記の第 3 回です。今回からは、気象庁の天気予報を題材にします。


ノーツで作るポータル画面に天気予報を表示できないかとネットを調べていると、気象庁の Web サイトから取得できることがわかりました。

今回はその調査結果をレポートします。


天気予報データの取得

次のリンクをクリックすると気象庁の天気予報の Web ページが確認できます。

気象庁 天気予報(大阪)


このサイトから天気予報データを抽出できる構造になっているようです。次の URL をクリックすると、天気予報のデータがテキストデータ(JSON 形式)で表示されます。これをノーツで利用しようということですね。

https://www.jma.go.jp/bosai/forecast/data/forecast/270000.json


また、天気予報以外では、明後日までと1週間の天気概況がテキストデータで取得できます。

https://www.jma.go.jp/bosai/forecast/data/overview_forecast/270000.json

https://www.jma.go.jp/bosai/forecast/data/overview_week/270000.json


なお、これら URL はリクエストした時点での最新情報が返される仕様になっており、固定された URL でいつでもデータ取得が可能です。


JSON の確認

それでは、天気予報の JSON を確認しましょう。表示されたテキストを VS code で開き、JSON として成形して構造を確認します。

まず大枠として、2つのオブジェクトがあり、明後日までの天気予報と週間天気予報があります。

明後日までの天気予報はさらに3分割されていて、天気、降水確率と気温の予報が入っています。

天気予報のオブジェクトは、"timeDefines" に予報日時が配列で入っており、各予報が同じ要素数の配列で記録されています。

なお、この例では翌日の予報までしか含まれていませんが、JSON を取得する時刻によっては明後日を含む場合があります。コーディングする際には、このあたりを柔軟に行う必要があそうですね。


降水確率のオブジェクトも同様の構造になっていますが、"timeDefines" が天気予報とは合致していません。降水確率は 6 時間ごとの予報が提供されるようです。


気温予想に関しては、日ごとの最低/最高気温が取得できます。ただ、当日の最低気温は通常不要です。Web ページでも以下のように省略されています。

JSON では、以下のようにデータが存在しているのですが、最高気温と同じ数値が入っていて、配列の並び順が逆転しています。このあたりで省略する条件を表していると想定しているのですが、不明瞭ですね。仕様がわからないので判断が難しいところです。


週間天気予報についても同様の構造になっていて、7 日分の天気予報だけでなく、降水量や平均気温まで、Web ページに表示されている情報が含まれています。


エリアコード

URL の最後の数字部分が取得するエリアのコードです。例えば、東京の情報を取得するなら 130000.json となります。エリアの定義は、以下の URL の JSON で確認できます。

https://www.jma.go.jp/bosai/common/const/area.json

天気予報の上記3 URL では、”offices” に定義されているエリアコードが利用できるようです。


まとめ

今回は、気象庁の Web サイトから天気予報を取得する方法についてまとめました。

ただ、気象庁として正式な API サービスではないとのことなので、仕様変更や最悪提供中止があるかもしれません。とはいっても、特定の URL にアクセスすると JSON 形式で天気予報や概況が取得できる仕様になっていますので、インターフェース的には WebAPI といっても問題ないと思います。

正式なサービスではないため、仕様が公開されていません。手探りの部分があり、不明瞭な点は存在はしますが、必要十分な情報は取得できそうです。とはいっても、公的は Web サイトから取得できるのはありがたいですね。

なお、以下のリンクに利用規約などが記載されております。実用前にはご確認ください。

気象庁ホームページについて


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

2023/12/04

NRPC ポートの接続確認

Domino サーバと Notes クライアント、Domino サーバ間の通信では、一般的に NRPC(Notes Remote Procedure Call)を使用します。NRPC は通常 1352 のポートを使用します。

追加のサーバやクライアントのセットアップをしていると、Ping は通るのにサーバに接続できない症状に出くわすことがあります。原因は Windows ファイアウォールなどにより NRPC ポートで通信できないことによるのですが、これが把握できないと、ネットワーク的な問題か Notes/Domino の問題か切り分けできません。

そこで今回は、NRPC での疎通確認を Notes を使わずに行う方法を整理します。


Telnet の利用

コマンドプロンプトを起動して以下のコマンドを入力します。[IP address] は接続確認を行うサーバの IP アドレスです。

   telnet [IP address] 1352


接続できない場合は、エラーが表示されます。


接続できた場合は、次のようにウィンドウタイトルが "Telnet" となり真っ黒な画面となります。

この画面が確認できれば、指定したサーバに対して NRPC で接続できることになります。


Telnet の設定

上記の Telnet を使用するには、最近の Windows では追加の設定が必要です。

Windows 10 の場合、コントロールパネルの [プログラムと機能] を開き [Windows の機能の有効化または無効化] をクリックします。

表示された [Windows の機能] ダイアログのリストから『Telnet クライアント』にチェックを入れます。

これでコマンドプロンプトで Telnet コマンドが利用可能となります。


Windows PowerShell の利用

スタートメニューより Windows PowerShell を起動し、次のコマンドを入力します。もちろん、[IP address] は接続確認を行うサーバの IP アドレスです。

   Test-NetConnection -ComputerName [IP address] -Port 1352


接続できない場合は、警告メッセージが表示され、テスト結果の TcpTestSucceeded が False と出力されます。


接続できた場合は、TcpTestSucceeded が True となります。


接続確認時の注意

NRPC のポートは Domino サーバが起動しないと開きません。起動前にテストしても、今回の確認は、すべてエラーとなりますので、ご注意ください。

2023/12/02

命名規則の例(LotusScript)

先日投稿した ビジネスアプリ開発で大切なこと の記事の中で、コーディングルールについて紹介しました。その具体例として、今回は、命名規則についてまとめます。

命名規則は、プログラム内の変数や関数、フォームやビューなどの設計要素など用途は様々ですが、今回は一番わかりやすい LotusScript の変数宣言に絞って記述します。

ほとんどの開発者は、ご自身で何らかのルールに基づき命名されていると思います。学生の頃、すべての変数を某ガン〇ムのモビルスーツ名を変数にしているプログラムに出くわしたことがあり、変数の用途を理解するのに四苦八苦したことを覚えています。しばらくして、数が足りなかったのでしょうね、途中からキャラ名に切り替わっており、ますます混とんとしたコードになっていました...

ビジネスでは、このようなことがないよう、他のメンバーでもガ〇ダムにくわしくない人でもわかりやすい名前を付ける必要があります。

その一例として、私が通常使用している命名規則を紹介します。


命名規則の構成

変数は、値やオブジェクトを一時的に保管しておく器のようなものです。命名するときはその変数の中身が想像できるような名前を付けます。短い名前で用途が明確になるとすっきり、わかりやすいプログラムになります。

私が利用している命名規則では、1つの変数名は4つのパートに分けて考えます。

[接頭語][タグ][基本名][接尾語]

各パートの用途や役割は次の通りとしています。

接頭語 スコープやコーディング上の用途を指定
タグ 変数の型を指定
基本名 変数名の本体
用途や役割を簡潔に表した一つまたは複数の単語で構成
接頭語やタグと区別するため単語の大文字で始める
接尾語 類似する変数が複数必要となるなど、基本名が重複する場合に指定
アンダースコアを使って、基本名と区別するとわかりやすい

ただ、このルールを必須とするとすべての変数が長くなり、コードが見づらくなります。そこで、ループ変数(ex. i, n など)のようにコーディング上短い変数名の方が可読性が上がる場合は例外としています。

また、変数の数が少ない場合は、タグだけでもコードの理解に支障はないので、基本名も必須とはしてません。要は、変数の用途がわかれば OK という比較的 ”ゆるい” ルールとしています。


接頭語

スコープは、その変数が有効な範囲のことです。例えば、スクリプトライブラリ内に宣言された変数が Public の場合、そのライブラリを利用するプログラムからアクセスできます。これは、外部から値が変更されることがあり得ることになり、この変数を利用する場合に注意する必要があります。

関数の引数の場合、値が関数内でセット(または変更)されるのか、呼び出し元から値提供されるだけなのかを明示すると仕様がわかりやすくなります。入出力インターフェースを明確にする効果があり、関数の利用者に仕様を伝えやすくなります。

このように、変数がプログラム上でどのようなふるまいをするのか指定する部分が接頭語となります。


なお、スコープや関数の引数に関しては、上記の説明だけではわかりにくいですよね。別の機会に掘り下げて説明したいと思います。


タグ

タグは変数の型を表します。

例えば、コードを表す変数を定義する場合、sCD だと文字列(string)、iCD だと数値(integer)が入っていると表現できます。Notes Object Class の場合であれば、NotesDocument は nd、NotesDatabase は ndb と決めておけば、その変数の中身(値の種類)が明確になります。

このタグに当たる部分は、実践されている方は多いかと思います。


基本名

変数の用途や役割を表す部分です。先ほどのコードを表す変数 sCD を例にすると、CD が基本名となります。

末尾で紹介する命名規則では、各パートの役割と並び順を規定するにとどめており、基本名の付け方までは規定していません。ただ、この基本名は開発者によりばらけやすく、用途を不鮮明になりやすい部分です。

例えば、”事業所” を表す変数を考えます。変数名を英語でつける場合には、次のような単語が想定できます。

  • office
  • shop
  • location
  • place
  • branch

ただ、その企業にとって ”事業所” という言葉は工場や営業所を含む総称であった場合、そぐわない単語もあります。また、ローマ字で Jigyosho するかもしれませんし、Jigyousyo とする開発者がいるかもしてません。開発メンバーに外注が入るとこのような事象が起こりやすいといえます。

これを避けるため ”事業所” は "JIS" を基本名とすると事前に決めておき共有することで、的外れな命名やブレを抑制できます。社内で共通認識がある単語の場合、用語集のような命名表を事前に用意しておくのもいいかもしれませんね。


接尾語

同様の用途の変数を複数作成する場合、基本名までが同じとなります。

例えば、ドミノディレクトリのユーザ文書をビューから順に取得するプログラムを作っていたとします。処理中の文書が ndUser という変数だったとすると次の文書を保存する変数を定義するなら ndUser_Next、 前の文書を表す変数は  ndUser_Prev とします。この ”_” 以降が接尾語となります。


命名規則のサンプル

このブログを書くにあたり使用している命名規則を別途まとめました。特に重要視している接頭語とタグについて整理しています。

この投稿と重複する部分もありますが、以下のリンクより確認ください。

命名規則:LotusScript (変数)


例えば、先日投稿した『Notes - Excel 連携:#26)罫線の設定 ②』という記事で作成した関数を例に挙げると、変数を見るだけで次のような情報が判別できます。


今後掲載するサンプルコードもこの規則に従い作成しますので参考にしてください。なお、このリンク先の命名規則は、将来改変する可能性があります。場合によっては、この記事と食い違うことになるかもしれません。予めご了承ください。


2023/11/30

チーム開発で大切なこと

ノーツは一般にビジネスで使用するアプリケーションです。そこでアプリケーションを開発される方はほとんどの場合 ”仕事” としてプログラミングされていると思います。

では、”仕事” でアプリケーションを開発する場合、どのようなことに気を付けるべきでしょうか?

ポイントは、趣味の開発とは違って、複数人で担当することがあるという点だと思います。たとえ現在一人で開発していたとしても、将来担当替えなどで誰かに引き継ぐかもしれません。この点を考慮に入れると、アプリ開発が一人で完結することは稀なはずです。

他のメンバーと効率よく作業できる ”チーム開発” が、”仕事” での開発に重要と考えており、その結果、生産性や開発物の品質の向上につながると考えています。

私が重視しているのは次の3点です。

  • わかりやすいこと
  • 効率が良いこと
  • メンテナンス性が高いこと

それぞれの項目について掘り下げてみましょう。


わかりやすいこと

簡単に言うと「プログラムが読みやすい」ということです。

スペースやタブを活用して整然と記述します。As や = などの位置がそろっていると気持ちいですよね。処理が複数のブロックに分かれる場合には、その間に改行を入れ間隔をあけるのも効果的です。

このように、プログラムのレイアウトを整え、見た目に美しいプログラムにするだけで、ずいぶんわかりやすくなります。汚い字で書かれた手書きメモは、読む前から気分がそがれるのと同じですね。

また、変数や関数名を適切に命名し、見ただけで用途がわかるようにします。必要に応じてコメントを記述すると、さらにわかりやすくなります。

チームの他のメンバーでもわかりやすいかを意識してコーディングするといいですね。これは技術ではなく ”思いやり” になりますね...


効率が良いこと

処理時間が短い、データの更新が少ない、メモリの使用量が少ない、プログラムが短いなど、”効率” にはさまざまな指標があります。ただ、こういった指標を追求したプログラムは得てして複雑になったり、読みにくくなります。例えば、変数を使いまわしたり、複雑なアルゴリズム利用したり、コメントを省略したりなどです。

ノーツのようなビジネスアプリケーションの場合は、これらの指標を追求するより、前項のわかりやすさを優先すべきかと思います。まずは仕様通りのプログラムをわかりやすく記述し、どうしてもレスポンスが悪いなど不都合がある部分だけをチューニングすればいいでしょう。

後任者がわかりやすいことがビジネス上の ”効率が良い” といえますよね。将来、すっかり忘れた自分自身が得するかもしれませんしね(笑)


メンテナンス性が高いこと

これは将来の仕様変更などプログラムの修正がしやすいということになります。

古い言い方ですが、ノーツは EUC(End User Computing)のツールなので、アプリケーション開発といっても、プログラマではない方が担当することが多々あります。そのため、アプリを動かすことだけに注力し、メンテナンス性まで意識できていない場合が多いといえます。ただ、最近の流れの早いビジネス環境に対応するためにはメンテナンスしやすい状態にしておくことが大切だと思います。


どうすればいいの?

これらを具体的に実現するためには、ソフトウェア工学という学問や、構造化プログラミング、オブジェクト指向などの技法や考え方を駆使する必要があります。私はその筋のプロではありませんので、そんなことは解説できません。

真似事かもしれませんが、私が実践していることは次のような項目です。参考までにご紹介します。主に LotusScript を意識していますが、@関数でも応用できることはあるかと思います。

  • コーディングスタイル(レイアウトのルール)を決定
  • コメントを適切に記述する
  • 命名規則を決定する
  • 変数を宣言する(暗黙の変数宣言を禁止する)
  • パブリック変数を(極力)使用しない
  • 無用なスコープは与えない
  • 共通の処理は関数化する
  • 関数のインターフェースを明確化する
  • 汎用的な関数はライブラリ化する
  • 一般的な処理は部品化する(@関数やダイアログボックス)


こういったことをルール化することを、コーディング規約やコーディングルール、標準化などと言います。ルールは制定するだけではだめで、実践することが大切です。チーム全員で実践することで、分担作業がしやすくなり、生産性が上がります。また、他人のプログラムが読みにくいという問題も改善できます。

ライブラリ化を進めると、ライブラリ開発とアプリ開発を分担したり、アプリ開発者はアプリの機能に集中できるという効果もあります。ノーツでは書かない場合も多いのですが、仕様書のドキュメント量を減らすこともできます。将来、担当替えがあったときは引継ぎ工数が削減できるはずです。


これらすべてを初めから完成させるのは大変です。ルールを作ることだけでなく、ルールを守ることにも手間(コスト)が発生します。実現することより、必要性をチームで認識する方が重要です。できることから順にはじめ、効果を感じられたら範囲を広げるなど、段階的に進めるぐらいで十分だと思います。


まとめ

転職などもあり、これまでに複数のチームで開発をしてきました。お客様の要望や上司の指示によりそれぞれの環境で、チームの生産性向上に取り組んできました。その経験を事例としてご紹介しました。

多岐にわたるため、今回は概要の説明だけとなりました。これだけでは、『だから??』となるかと思います。今後、より具体的に事例を紹介する機会を作る予定です。

2023/11/28

Notes - Excel 連携:#27)セルの背景色や文字色の設定

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

帳票のヘッダ部分の装飾を題材に、前回紹介した罫線以外のセルの装飾についてまとめます。今回もセルを取り扱いますので、起点となるオブジェクトは Range となります。


背景色の設定

Range オブジェクトには Interior プロパティが存在します。このプロパティから取得できる Interior オブジェクトで背景色の設定を行うことができます。

Interior オブジェクト (Excel)

Microsoft Learn で仕様を確認するとプロパティしか定義されいません。主なプロパティは次の通りですが、背景色をセットするためのオブジェクトと理解して差しさわりないようですね。

プロパティ 説明
Color 背景色
Pattern 網掛けのパターン
(設定値については XlPattern 列挙 を参照)
PatternColor 網掛けの色


文字の色と装飾

セル内の文字の装飾は Range オブジェクトの Font プロパティから Font オブジェクト を取得できます。

この Font オブジェクトに色を設定する Color プロパティが存在します。他には、太字(Bold)や下線(Underline)などの装飾、第 21 回 で利用したフォント(Name)や文字サイズ(Size)も存在します。


文字揃え

セル内の文字揃えは Range オブジェクトの HorizontalAlignment プロパティ で行います。設定できる値は XlHAlign 列挙 に定義されています。主な値は次の通りです。

定数 説明
-4131 xlHAlignLeft 左揃え
-4108 xlHAlignCenter 中央揃え
-4152 xlHAlignRight 右揃え
1 xlHAlignGeneral データの種類に従って揃える

xlHAlignGeneral は Excel らしい設定ですね。これらの定数はスクリプトライブラリ lsXls に登録しておきましょう。


表のタイトル部分の設定

タイトル行背景をグレーに設定し、文字を白色に設定します。ヘッダ部分を設定する関数 xDrawHeader を新規で作成します。関数の引数は、Worksheet オブジェクトで、メインルーチン次のようになります。

Function xDrawHeader(voSheet As Variant)
   Dim oRange As Variant
   Dim s As String

   'ヘッダ行(背景と文字色)
   s = GetRangeString(xciHeaderRows, 2, xciHeaderRows, 9)
   Set oRange = voSheet.Range(s)
   oRange.Interior.Color = RGB(128, 128, 128)
   oRange.Font.Color = RGB(255, 255, 255)

End Function

続いて数値項目のタイトルを右寄せにします。数値項目は C 列 ~ I 列なので次のようになります。

   'ヘッダ行(揃え)
   s = GetRangeString(xciHeaderRows, 3, xciHeaderRows, 9)
   Set oRange = voSheet.Range(s)
   oRange.HorizontalAlignment = xlHAlignRight

ここまでを実行すると、以下のようにタイトルと数値の寄せがずれる現象が発生します。

これは、初期化処理で数値のセルで次のように指定したからです(第 22 回 参照)。最後のアンダースコアとスペースが右側に1文字分スペースを入れる設定になります。

   voSheet.Range("C:I").NumberFormatLocal = "#,##0_ " '数値

同様の設定は文字列のセルでも可能です。次の行を追加すると揃えることができます。

   oRange.NumberFormatLocal = "@_ "


出力日時の表示

2行目の出力日時の表示は次のように記述します。値としては現在時刻をセットしているだけで、Excel の書式と揃えで見た目を整えています。

   '出力日時
   Set oRange = voSheet.Cells(2, 2)
   oRange.HorizontalAlignment = xlHAlignRight
   oRange.NumberFormatLocal = |yyyy/m/d hh:mm "出力"|
   oRange.Value = Now


帳票タイトルの表示

同様に帳票のタイトルの設定を行います。タイトルは、文字を大きく、太字と下線を設定しています。タイトルも出力日時と同様に書式を使って

   '帳票タイトル
   Set oRange = voSheet.Cells(3, 2)
   oRange.HorizontalAlignment = xlHAlignCenter
   oRange.NumberFormatLocal = |"プリンタ出力費用一覧("yyyy"年"m"月)"|
   oRange.Font.Size = 16
   oRange.Font.Bold = True
   oRange.Font.Underline = True
   oRange.Value = oRange.Value

タイトルも出力日時と同様に書式を使って表示を整えています。書式は値をセットする前に設定しておかないと反映されないようです。一番下の『oRange.Value = oRange.Value』で自分自身の値を代入することで、書式を有効化しています。


なお、上記コードは帳票タイトルのセルに出力年月が日付値で入っている前提となっています。そのため、第 23 回 で作成した xPrintUsages 関数に以下のコードを追加しておく必要があります。

   '帳票タイトル用に年月を日付値でセット
   voSheet.Cells(3, 2).Value = DateNumber(viYear, viMonth, 1)


まとめ

これで、帳票の見た目部分は完成となります。実行すると次のようなシートが出力されます。

ノーツのデータから Excel で帳票を出力する方法を紹介しました。さまざまな Excel のオブジェクトを利用しましたが、そのほとんどが Range オブジェクトを起点としていました。

Worksheet オブジェクトの Cells や Range プロパティ、Rows や Columns プロパティなどを使用して Range オブジェクトの取得方法とその Range オブジェクトからどのような操作ができるのかを習得することが重要ですね。


前回 Notes - Excel 連携 次回

2023/11/26

Notes - Excel 連携:#26)罫線の設定 ②

前回は、罫線の設定に必要な情報収集とライブラリの準備を行いました。今回は、これらを利用して、作成中の帳票に罫線を設定します。


ライブラリの組み込みと重複排除

まずは前回作成したスクリプトライブラリ lsXls を作成中のエージェントに組み込みます。

ライブラリに移行した関数 xRCToA1 と x9ToA は不要なので削除します。すると、関数名が変わっているので文法エラーが発生します。ライブラリの関数名 RCToA1 にすべて修正します。

xCalcSum 関数内のエラーについては、前回新規作成した関数 GetRangeString に置き換えます。


oRange.Formula = "=Sum(" & GetRangeString(iMin, viCol, iMax, viCol) & ")"


外側の罫線作画

いよいよ罫線の作画を行います。

作画の処理は、専用の関数 xDrawLine に記述します。関数の引数は、Worksheet オブジェクトと明細行の行数とします。メインルーチンは次のようになります。

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

   '罫線の設定
   Call xDrawLine(oSheet, iDoc)

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


まずは、帳票の外側に枠線を設定します。次のように記述すると上側を

  • スタイルは ”実線”
  • 幅は ”普通”
  • 色は ”グレー”

に設定します。

Function xDrawLine(voSheet As Variant, ByVal viDoc As Integer)
   Dim oRange As Variant
   Dim oBorder As Variant

   Dim s As String
   Dim lCol As Long
   Dim iTop As Integer
   Dim iBottom As Integer

   iTop = xciHeaderRows
   iBottom = xciHeaderRows + viDoc + 1

   '色の準備
   lCol = RGB(128, 128, 128)

   '表全体
   s = GetRangeString(iTop, 2, iBottom, 9)
   Set oRange = voSheet.Range(s)

   '外枠(上)の設定
   Set oBorder = oRange.Borders(xlEdgeTop)
   oBorder.Weight = xlMedium
   oBorder.LineStyle = xlContinuous
   oBorder.Color = lCol

End Function

帳票全体にあたるセルの範囲を oRange 変数に取得し、その上側の枠線を oBorder 変数にセット。そのオブジェクトのプロパティの罫線のスタイルと幅、色を設定しています。

これを上下左右の4辺に対して設定する必要があるのですが、幸い XlBordersIndex 列挙 において 7 ~ 10 の連続した値が割り当てられています。これを利用すると次のようにループ処理でシンプルに記述できます。

   '外枠の設定
   For i = xlEdgeLeft To xlEdgeRight
      Set oBorder = oRange.Borders(i)
      oBorder.Weight = xlMedium
      oBorder.LineStyle = xlContinuous
      oBorder.Color = lCol
   Next


内側の罫線の作画

内側の罫線は、まずベースとなる実線の最も細い線、色は同じくグレーで初期設定します。内側の罫線には、縦と横の設定がありますが、こちらも XlBordersIndex 列挙 で連続した値となっています。ですので、今回もループで対応します。

   '内側のベース罫線の設定
   For i = xlInsideVertical To xlInsideHorizontal
      Set oBorder = oRange.Borders(i)
      oBorder.Weight = xlHairline
      oBorder.LineStyle = xlContinuous
      oBorder.Color = lCol
   Next


仕上げとして、初期化した罫線とは違う部分を順に設定します。

例えば、ヘッダ行の下の罫線を2重線に設定するには、そのエリアの Range オブジェクトを取得して、罫線を設定します。範囲が違うだけで手順は同じですね。

   'ヘッダ行
   s = GetRangeString(iTop, 2, iTop, 9)
   Set oRange = voSheet.Range(s)
   Set oBorder = oRange.Borders(xlEdgeBottom)
   oBorder.LineStyle = xlDouble


同様の方法で他の罫線も設定します。

最終行となる合計行の上部を2重線に設定、縦罫線は白黒とカラーの間を極細、最終列の合計を2重線に設定します。

   '合計行
   s = GetRangeString(iBottom, 2, iBottom, 9)
   Set oRange = voSheet.Range(s)
   Set oBorder = oRange.Borders(xlEdgeTop)
   oBorder.LineStyle = xlDouble

   '縦罫線
   s = GetRangeString(iTop, 3, iBottom, 5)
   Set oRange = voSheet.Range(s)
   Set oBorder = oRange.Borders(xlEdgeLeft)
   oBorder.Weight = xlThin
   Set oBorder = oRange.Borders(xlEdgeRight)
   oBorder.Weight = xlThin

   '縦罫線(合計)
   s = GetRangeString(iTop, 9, iBottom, 9)
   Set oRange = voSheet.Range(s)
   Set oBorder = oRange.Borders(xlEdgeLeft)
   oBorder.LineStyle = xlDouble


実行結果

ここまでのプログラムを実行すると次のようになります。


前回 Notes - Excel 連携 次回

2023/11/25

Notes - Excel 連携:#25)罫線の設定 ①

第 20 回からスタートした『帳票の作成』シリーズの 6 回目です。今回は帳票を帳票らしく見せるために重要な要素である罫線についてまとめます。


設定方法

Excel で罫線を設定するには設定するセルを選択して『セルの書式設定』画面を開き、[罫線]のタブから設定します。

セルを選択して設定する操作を VBA で行うにはどうするでしょうか?

そうです。セルの範囲に対する操作ですから、Range オブジェクトを使うということになりますね。その Range オブジェクトには罫線を操作するためのプロパティ Borders があります。

Range.Borders プロパティ (Excel)

このプロパティは引数が1つあり、どの部分の罫線を取得するかを指定する必要があります。指定できる値は XlBordersIndex 列挙 として定義されていて、主な値は次の通りです。

定数 説明
7 xlEdgeLeft 範囲の左側の罫線
8 xlEdgeTop 範囲の上側の罫線
9 xlEdgeBottom 範囲の下側の罫線
10 xlEdgeRight 範囲の右側の罫線
11 xlInsideVertical 範囲の外側を除くすべての垂直罫線
12 xlInsideHorizontal 範囲の外側を除くすべての水平罫線


罫線のプロパティ

Borders プロパティの型は Borders オブジェクト となります。このオブジェクトに罫線のスタイルを設定するプロパティが存在します。今回使用するのは次の3つです。

プロパティ 説明
LineStyle 罫線のスタイル
Weight 罫線の幅
Color 罫線の色


LineStyle はその名の通り、実線や破線などの罫線の種類です。XlLineStyle 列挙 として定義されており、代表的なものは次の通りです。

定数 説明
1 xlContinuous 実線
-4119 xlDouble 2 本線
-4115 xlDash 破線
-4142 xlLineStyleNone なし


Weight は罫線の太さですが、次の4種類が XlBorderWeight 列挙 として定義されています。

定数 説明
1 xlHairline 細線 (最も細い罫線)
2 xlThin 極細
-4138 xlMedium 普通
4 xlThick 太線 (最も太い罫線)


Excel の UI とプロパティの関係

セルの書式設定の「線」の選択肢では、複数のプロパティを一度に設定するようになっています。例えば、罫線のスタイル(LineStyle)を xlContinuous に限定すると次のように4つすべての太さが表示されています。

同様に「プリセット」の外側では、選択範囲の外側(XlBordersIndex 列挙 の上下左右)を一括設定できます。このように、Excel の UI では、複数のプロパティをまとめて設定するようになっていて、操作性を向上させています。


罫線の色の指定

今回はテーマを使用せず、直接色を指定する方法で作成します。Excel の VBA には RGB という関数存在します。この関数の戻り値を Borders オブジェクトの Color プロパティにセットすると罫線の色が設定できます。

RGB 関数

この関数を LotusScript で作成すると次のようになります。

Public Function RGB(_
              ByVal vbyR As Byte, ByVal vbyG As Byte, ByVal vbyB As Byte) As Long
    RGB = vbyR + CLng(vbyG) * 256 + CLng(vbyB) * 256 ^ 2
End Function


スクリプトライブラリの更新

上記の RGB 関数や前回作成の行と列を A1 形式に変換する関数は、この先何度も使用することが想定されます。そこで、これら関数をスクリプトライブラリに追加します。

グラフの作成で利用したスクリプトライブラリに追加します(#16, #17, #18)。なお、今回より、スクリプトライブラリ名を lsXls に変更していますのでご注意ください。

現時点でのスクリプトライブラリのコードは次の通りとなります。

なお、スクリプトライブラリ化にあたり、一般的に利用しそうな関数は Public として定義しています。また、Range プロパティでセルの範囲を指定する文字列(ex. A3:H12)を作成する関数 GetRangeString を追加しています。

Option Declare

'XlAxisType 列挙 (Excel)
Public Const xlCategory = 1
Public Const xlValue = 2

'XlCategoryType 列挙 (Excel)
Public Const xlAutomaticScale = -4105
Public Const xlCategoryScale = 2
Public Const xlTimeScale = 3

'MsoChartElementType 列挙 (Excel)
Public Const msoCategoryGridLinesMajor = 334
Public Const msoValueGridLinesMinorMajor = 331
Public Const msoCategoryGridLinesMinorMajor = 335

'XlBordersIndex 列挙 (Excel)
Public Const xlEdgeLeft = 7 '範囲の左側の罫線
Public Const xlEdgeTop = 8 '範囲の上側の罫線
Public Const xlEdgeBottom = 9 '範囲の下側の罫線
Public Const xlEdgeRight = 10 '範囲の右側の罫線
Public Const xlInsideVertical = 11 '範囲の外側を除くすべての垂直罫線
Public Const xlInsideHorizontal = 12 '範囲の外側を除くすべての水平罫線

'XlBorderWeight 列挙 (Excel)
Public Const xlHairline = 1 '細線 (最も細い罫線)
Public Const xlMedium = -4138 '普通
Public Const xlThin = 2 '極細
Public Const xlThick = 4 '太線 (最も太い罫線)

'XlLineStyle 列挙 (Excel)
Public Const xlContinuous = 1 '実線
Public Const xlDouble = -4119 '2 本線
Public Const xlLineStyleNone = -4142 'なし
Public Const xlDash = -4115 '破線

Public Function GetRangeString(_
      ByVal viRowFm As Integer, ByVal viColFm As Integer, _
      ByVal viRowTo As Integer, ByVal viColTo As Integer) As String
   Dim s As String

   s = RCToA1(viRowFm, viColFm)
   s = s & ":" & RCToA1(viRowTo, viColTo)

   GetRangeString = s
End Function

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

Public Function RGB(_
      ByVal vbyR As Byte, ByVal vbyG As Byte, ByVal vbyB As Byte) As Long
   RGB = vbyR + CLng(vbyG) * 256 + CLng(vbyB) * 256 ^ 2
End Function

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


続きは次回

これで罫線の設定に必要な材料はそろいました。

ただ、少し長くなったので今回はここまででとします。次回は、このライブラリを使って実際に罫線を設定します。


前回 Notes - Excel 連携 次回