[ODC].xls形式のファイルを読み書きする（Copilotにコードを書いてもらう）

今でも、.xls形式のファイルを読み書きする需要があるようなので、とりあえず、１セルを読み書きする手順を確認してみる。

NPOIをExternal Logicに取り込んで動作確認した。機能を色々調べながら記事を書こうと思っていたが、Copilotがそのまま使えるコードを出してくれたので、使ったプロンプトとコードの紹介に留める。

# 環境情報
ODC Studio(Version 1.5.7)
[NPOI 2.7.2 (nissl-lab/npoi)](https://github.com/nissl-lab/npoi)

# NPOI
元々はJava用のOfficeファイル操作用APIの、POIを.NETに移植したもの。
検索してみると2つレポジトリが引っかかるが、ここでは、最終更新日がより新しいhttps://github.com/nissl-lab/npoi
の方を利用する。

ライセンスは、上記レポジトリによると、Apache-2.0。

# 機能はExternal Logicで提供する
.NETライブラリが提供する機能をODCで利用したい場合、基本的にはExternal Logicを利用する。
基本的な使い方は、

https://qiita.com/jyunji_watanabe/items/2698fb5a80b857875a48

VSCodeでプロジェクトを用意する手順は、

https://qiita.com/jyunji_watanabe/items/04ffd0bc25ed4e845661

を参照。
上記手順でプロジェクトを用意し、VSCocdeで開いた状態で作業する。

# .xlsファイルの特定セルを読み取るAction

## Copilotに渡したプロンプト
以下のプロンプトを、Copilotのチャットに渡す。「仕様」の部分で、作成したいメソッドの仕様を指定。

``` プロンプト
次の条件を満たすC#のメソッドを作成してください。

前提
- .NET 8
- OSはLinux
- ソリューション及びプロジェクトの作成は不要
- NuGetにあるライブラリを使って良い（必要なNuGetライブラリについては、VSCode上でプロジェクトに追加するためのコマンドリストを生成すること）
- メソッドはinterfaceで定義し、そのinterfaceを実装する
- interfaceとclassは別ファイルで定義する
- interfaceには「OSInterface」属性を付与する
- interface内のメソッドには「OSAction」属性を付与する
- OSInterface及びOSAction属性は別の場所で定義したものを参照するので、定義部分は出力しない

仕様
- NPOIをNuGetからインストールして利用する
- .xlsファイルのバイナリを受け取る
- シート名と、シート内で読み取りたいセルを特定する情報を受け取る
- 読み取った値を返す
```

## Copilotの出力
NPOIをNuGetからインストールする以下のコマンドは、VSCodeのTerminalタブかOSのコンソールで入力する。

``` :NPOIをインストールするコマンド
dotnet add package NPOI
```

出力されたコード部分。ざっと読んだ感じおかしなところはなく、試してみたら普通に動いた。
interfaceとclassをコピーし、用意しておいたプロジェクトのinterfaceとclass内に貼り付ける。

``` cs:interface
using System.Collections.Generic;
using OutSystems.ExternalLibraries.SDK;

[OSInterface(Description = ".xlsファイルのバイナリを受け取り、指定されたシート名とセル情報から値を読み取る機能を提供します")]
public interface IQiitaSamples
{
    [OSAction(Description = ".xlsファイルのバイナリを受け取り、指定されたシート名とセル情報から値を読み取る", ReturnName = "CellValue")]
    string ReadCellValue(byte[] fileBinary, string sheetName, string cellAddress);
}
```

class定義の「return cell.ToString();」部分は警告が出るので修正した方がいいかもしれない。
``` cs:class
using System;
using System.IO;
using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;

public class QiitaSamples : IQiitaSamples
{
    public string ReadCellValue(byte[] fileBinary, string sheetName, string cellAddress)
    {
        try
        {
            using (var stream = new MemoryStream(fileBinary))
            {
                var workbook = new HSSFWorkbook(stream);
                var sheet = workbook.GetSheet(sheetName);
                if (sheet == null)
                {
                    throw new ArgumentException($"Sheet {sheetName} not found.");
                }

                var cellReference = new NPOI.SS.Util.CellReference(cellAddress);
                var row = sheet.GetRow(cellReference.Row);
                if (row == null)
                {
                    throw new ArgumentException($"Row {cellReference.Row} not found.");
                }

                var cell = row.GetCell(cellReference.Col);
                if (cell == null)
                {
                    throw new ArgumentException($"Cell {cellAddress} not found.");
                }

                return cell.ToString();
            }
        }
        catch (Exception ex)
        {
            throw new ArgumentException("Error reading cell value.", ex);
        }
    }
}
```

プロジェクトをpublish→zip→ODC PortalでPublish→TestかReleaseしてConsumer Appから利用できるようにする。

## 上記メソッドに対応するAction
以下のActionができる。Excel97-2003形式のファイルを使って、ODCのAppから動作確認すると問題なく動作した。
- Input Parameter
    - fileBinary: Excelファイルのバイナリ（Upload Widgetでアップロードするなどして用意する）
    - sheetName: 読み取りするセルが存在するシートの名前（例：Sheet1）
    - cellAddress: 読み取りするセルを示すセル参照（例：A10
- Output Parameter
    - CellValue: 読み取った値（Text型）
![スクリーンショット 2025-02-07 20.59.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/c15254b1-5bef-7ecf-4b94-bc414594dcce.png)


# .xlsファイルの特定セルに書き込むAction
## Copilotに渡したプロンプト
``` プロンプト
上でやったのと同じ条件で、今度は以下のメソッドを作ってください。 

仕様 
- NPOIを使用
- .xlsファイルのバイナリを受け取る 
- シート名と、シート内で書き込みたいセルを特定する情報、書き込みたい値を受け取る 
- 書き込んだ結果をバイナリに戻して返す
```

## Copilotの出力
interface/class定義ごと出力されるが、必要なのはメソッド部分だけなので、
- interfaceからは、[OSAction]とそれに続くメソッド宣言の行をコピーし、読み込み機能で作成済みのinterface定義内に貼り付け
- 同様に、class内のメソッド定義を作成済みのclass内に貼り付け
- 機能が2つになるので、interfaceに付与している属性のDescriptionをふさわしいものに変えた方がいいだろう
``` cs:interface
using System.Collections.Generic;
using OutSystems.ExternalLibraries.SDK;

[OSInterface(Description = ".xlsファイルのバイナリを受け取り、指定されたシート名とセル情報に値を書き込む機能を提供します")]
public interface IQiitaSamples
{
    [OSAction(Description = ".xlsファイルのバイナリを受け取り、指定されたシート名とセル情報に値を書き込み、結果をバイナリとして返す", ReturnName = "UpdatedFileBinary")]
    byte[] WriteCellValue(byte[] fileBinary, string sheetName, string cellAddress, string value);
}
```

``` cs:class
using System;
using System.IO;
using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;

public class QiitaSamples : IQiitaSamples
{
    public byte[] WriteCellValue(byte[] fileBinary, string sheetName, string cellAddress, string value)
    {
        try
        {
            using (var stream = new MemoryStream(fileBinary))
            {
                var workbook = new HSSFWorkbook(stream);
                var sheet = workbook.GetSheet(sheetName);
                if (sheet == null)
                {
                    throw new ArgumentException($"Sheet {sheetName} not found.");
                }

                var cellReference = new NPOI.SS.Util.CellReference(cellAddress);
                var row = sheet.GetRow(cellReference.Row) ?? sheet.CreateRow(cellReference.Row);
                var cell = row.GetCell(cellReference.Col) ?? row.CreateCell(cellReference.Col);

                cell.SetCellValue(value);

                using (var outputStream = new MemoryStream())
                {
                    workbook.Write(outputStream);
                    return outputStream.ToArray();
                }
            }
        }
        catch (Exception ex)
        {
            throw new ArgumentException("Error writing cell value.", ex);
        }
    }
}

```

## 上記メソッドに対応するAction
以下のActionができる。Excel97-2003形式のファイルを使って、ODCのAppから動作確認すると問題なく動作した。
- Input Parameter
    - fileBinary: Excelファイルのバイナリ（Upload Widgetでアップロードするなどして用意する）
    - sheetName: 書き込み先セルが存在するシートの名前（例：Sheet1）
    - cellAddress: 書き込み先セルを示すセル参照（例：A10）
    - value: 書き込む値
- Output Parameter
    - UpdatedFileBinary: 書き込み後のExcelファイルのバイナリ

![スクリーンショット 2025-02-07 21.55.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/c61e0ff2-0a64-1b16-57cc-bc64b28b4d54.png)

# 今後の対応
上では、NPOIを使って、.xls形式ファイルの操作ができる事を確認するため、最小限の機能のみを実装した。

実際に使うには、要件に応じた様々な機能を追加しなければならない。Copilotが思ったよりいい感じにコードを書いてくれるので、自分でコードを書かなくてもかなりのことができそうだ。

External LogicはOutSystems 11のExtensionと違い、１回の呼び出しがHTTPS通信になりオーバーヘッドが大きい。そのため、OutSystems11の頃よりも、「多くのセルを一度に操作する」「多くの機能を一度に実行する」ことの重要性が大きい点に注意。

前者についてはCopilot作成できそうな気がする。後者は例えば、操作内容（機能名とパラメータ）を文字列で表し、そのListを渡すことで、一連の操作を一度に実行する機能の開発することが考えられる。こちらはチャットではなかなか難しそうにも思える。