LoginSignup
4
1

More than 3 years have passed since last update.

F#(fsi)を使ったExcel操作の覚え書き(実例編)

Last updated at Posted at 2021-03-11

準備編に続いて実例編に進みます。

まずはセル範囲の操作

準備編では単独セルの操作まで確認しましたが,実務ではセル範囲で扱うことが大半です。
サンプルとして,学校で使われそうな名簿データをCSVファイルで用意しました。当然ながら架空のデータです。
Excelで開いたら,任意のフォルダにExcel形式で保存してExcelを閉じてください。

ここでは"C:\Users\user1\Desktop\sample.xlsx"として保存したものとして進めます。
あらためて,fsiでExcelを起動し,ワークシートのデータを2次元配列dataとして取得します。

F# Interactive
#r "nuget: Microsoft.Office.Interop.Excel"
#r "nuget: Office"
open System
open System.Runtime.InteropServices
open Microsoft.Office.Interop.Excel
let ex = ApplicationClass(Visible = true) :> _Application
let path = @"C:\Users\user1\Desktop\sample.xlsx"
let wb = ex.Workbooks.Open(path) :> _Workbook
let ws = wb.Worksheets.[1] :?> _Worksheet
let data =
    ws.Range("A1").CurrentRegion.Value() :?> obj[,]
    |> Array2D.rebase
;;

今回は,後半でSystem.DateTimeを使いますので,open Systemでインポートしておきます。
また,COMオブジェクト解放時のコードを短くするためopen System.Runtime.InteropServicesも追加します。
<注意>
インポート宣言をする場合,上記の例のようにopen Microsoft.Office.Interop.Excelを最後(open Systemより後)にしてください。
.NET5(.NET Core)では,System.Range構造体が追加されたため,ExcelのRangeとメンバー名の衝突が起こります。
ダウンキャストで:?> Rangeを使用する可能性がある場合などは,上記の順序を守るか,openを使わずに完全修飾するなどの方法をとります。

セル範囲に対するValueプロパティは2次元配列obj[,]objで包んで返しますので,ダウンキャストして中身(obj[,])を取り出します。
中身である2次元配列のベースは[1,1]ですので,パイプ|>を通してArray2D.rebase関数に渡し,ベースを[0,0]にしておきます。
rebaseは任意です。データの形式や以降の処理に応じて使います。)

F# Interactive(結果)
val data : obj [,] = [["番号"; "名前"; "読み"; "性別"; "生年月日"]
                      [1.0; "浅井 清香"; "あさい さやか"; "女"; 2005/09/27 0:00:00]
                      [2.0; "安達 梓"; "あだち あずさ"; "女"; 2005/07/22 0:00:00]
                      [3.0; "安藤 麻由子"; "あんどう まゆこ"; "女"; 2005/04/12 0:00:00]
                      ・・・

以降は,このdataを利用します。

作業例

いよいよ,F#を「学校現場でありそうな作業」に活用してみます。

座席表

まずは,生徒40人を縦6列,右上(教室右前方)から番号順にした座席表を作ってみます。
次のコードをまとめてfsiに貼り付けてください。
プロセスが見えるように1行ごとに;;で実行し,itで受け取って次に渡しています。

F# Interactive
[ 1..40 ];;
it |> List.splitInto 6;;
it |> List.map (fun e -> if e.Length < 7 then e @ [ 0 ] else e);;
it |> List.rev;;
it |> List.transpose;;
it |> array2D;;

手順を示します。

  • 1~40の整数のリストを作ります (この先,生徒のインデックスとして使用)
  • 6分割します (2次元配列の元になる「リストのリスト」を作る)
  • 長さが足りないところは0を埋めます (完全な6行7列を作る)
  • 「行」を反転します (この後,行列転置するので実質は「列」の並びを反転)
  • 行列転置 (7行6列で右側の列が若い番号になる)
  • 「リストのリスト」をarray2D演算子で「2次元配列」にします
F# Interactive(最終結果)
val it : int [,] = [[35; 29; 22; 15; 8; 1]
                    [36; 30; 23; 16; 9; 2]
                    [37; 31; 24; 17; 10; 3]
                    [38; 32; 25; 18; 11; 4]
                    [39; 33; 26; 19; 12; 5]
                    [40; 34; 27; 20; 13; 6]
                    [0; 0; 28; 21; 14; 7]]

結果を見ると想定した座席配置ができていることがわかります。
fsiを使うと,途中のプロセスを確認しながら作業ができるので非常に助かります。

次の1行で2次元配列の各要素をインデックスにしてdataから氏名等のデータを転記して完成です。

F# Interactive
it |> Array2D.map (fun e -> if e <> 0 then $"{e, 2}\x0A{data.[e, 1]}" else null);;

インデックスに対して,補間文字列$"{data.[e, 0], 2}\x0A{data.[e, 1]}"を使って
  番号・セル内改行(16進 0A)・氏名
でセルの値を作ります。
また,インデックス「0」は,nullに置き換えます。
ValueまたはValue2プロパティにおいて,「空白セルの値」はnullであるためです。
以上,手順を分割して確認しました。

では,式としてまとめてみます。
ついでに,「空席」->「0」->「null」という二度手間を「空席」->「null」で済むように上記手順を入れ替えます。

F# Interactive
let zaseki =
    [ 1..40 ]
    |> List.map (fun e -> $"{e, 2}\x0A{data.[e, 1]}")
    |> List.splitInto 6
    |> List.map (fun e -> if e.Length < 7 then e @ [ null ] else e)
    |> List.rev
    |> List.transpose
    |> array2D
;;

新たに”座席表”ワークシートを作成し,結果を書き込みます。

F# Interactive
(wb.Worksheets.Add() :?> _Worksheet).Name <- "座席表"
(wb.Worksheets.["座席表"] :?> _Worksheet).Cells.Resize(7, 6).Value2 <- zaseki
;;

座席表ができました。

授業の際,「教壇側」から見た教員用の座席表も必要です。
ということで,今作ったzasekiを使って,視点を反転したzasekiRを作ってみます。

F# Interactive
let zasekiR =
    zaseki
    |> Seq.cast<obj>      // 2次元配列を平坦化
    |> Seq.rev
    |> Seq.chunkBySize 6  // もう一度6席ずつ切り分けて
    |> array2D
(wb.Worksheets.Add() :?> _Worksheet).Name <- "座席表(教員用)"
(wb.Worksheets.["座席表(教員用)"] :?> _Worksheet).Cells.Resize(7, 6).Value2 <- zasekiR
;;

はい,できました。
VBAでは得られない爽快感です。(個人の感想)

<おまけ>
「席替え」のように,ランダムに配置したい場合は

F# Interactive
let rand = Random()
[ 1..40 ] |> List.sortBy (fun _ -> rand.Next())
;;

こんな感じで1~40をランダムに並べ替えたリストができますので,これを使って座席表を作ります。

男女比を考慮したグループ分け

次の例です。
たとえば6班編制で清掃当番を作る場合,単なる名簿順では男女の構成に偏りが生じます。
トイレ清掃の担当時などに影響が出るので,男女それぞれを6分割して組み合わせてみます。

まずは,座席表同様にインデックスを作って男女別にまとめます。
List.partition関数で分離した女子・男子のリストをそれぞれfmとしてlet束縛します。

F# Interactive
let f, m = [ 1..40 ] |> List.partition (fun e -> data.[e, 3] = box "女");;

dataの各要素の型はobjです。
性別 "女" の判定式では型を合わせる必要があるのでbox "女"で「ボクシング(obj化)」します。

次にそれぞれのリストを6分割します。
List.splitInto関数は余りが出た場合に,リストの先頭側の要素数が大きくなります。
そのため,男女どちらか一方を降順にしてから分割し,末尾側の要素数が大きくなるようにします。
(ここでは女子を逆転させました。)

F# Interactive
let f = f |> List.rev |> List.splitInto 6 |> List.rev
let m = m |> List.splitInto 6
;;

続いて,6分割されたリスト同士を結合します。@はリスト結合演算子です。
結合後,班ごとに番号順にしておきます。

F# Interactive
(f, m) ||> List.map2 ( @ )
|> List.map List.sort
;;

男女比を考慮した6班ができました。
個人のインデックスからなるリストを6班分並べた「リストのリスト」になります。

40人を6班に分けるので,4つの班は7人,残り2班は6人になります。
座席表同様に,式でまとめるとこんな感じです。

F# Interactive
let seisou =
    let f, m = [ 1..40 ] |> List.partition (fun e -> data.[e, 3] = box "女")
    let f = f |> List.rev |> List.splitInto 6 |> List.rev
    let m = m |> List.splitInto 6
    (f, m) ||> List.map2 ( @ )
    |> List.map List.sort
;;

あとは,必要に応じて2次元配列を作成し,ワークシートに貼り付けます。

ワークシートへの貼り付けの一例

次のイメージのように,各班を並べたいと思います。
清掃班.png
班の区切りの罫線を引くために,各班の「人数(行数)」と「セル位置(行オフセット:赤矢印)」があると便利です。

このような処理は珍しくないので,使い回しできるように関数にしておきます。
引数として,行数(ここでは班の人数)のリストを渡します。

F# Interactive
let stackRows (rowsList : int list) =
    rowsList
    |> List.mapFold (fun state e -> ((state, e), state + e)) 0
    |> fst
;;

先ほどのseisouで確認してみます。まずは,各班の人数をリストにします。

F# Interactive
seisou |> List.map List.length;;
F# Interactive(結果)
val it : int list = [7; 7; 7; 6; 6; 7]

人数のリストを関数stackRowsに渡します。

F# Interactive
it |> stackRows;;
F# Interactive(結果)
val it : (int * int) list = [(0, 7); (7, 7); (14, 7); (21, 6); (27, 6); (33, 7)]

行のオフセットと行数のペアが完成しました。

次にセル範囲を罫線で囲む関数を用意します。
このように関数にしておくと,他でも使い回しがききます。

F# Interactive
let borderOutside (range : Range) =
    [
        XlBordersIndex.xlEdgeTop
        XlBordersIndex.xlEdgeBottom
        XlBordersIndex.xlEdgeRight
        XlBordersIndex.xlEdgeLeft
    ]
    |> List.iter
           (fun e ->
                range.Borders.[e].LineStyle <- XlLineStyle.xlContinuous
                range.Borders.[e].Weight <- XlBorderWeight.xlThin)
;;

VBAより見た目がいいと思います。(個人の感想)
注:関数として自由度を持たせましたが,セル範囲を同じ線種で囲む場合はRange.BorderAroundメソッドを使う方が一般的です。

準備が整いました。
セルH1を基準にして,ワークシートに清掃班を貼り付けます。

ここでは,2段階に分けて作業します。
最初に,氏名等のデータを貼り付け,内側の罫線を引きます。

F# Interactive
seisou
|> List.concat
|> List.map (fun e -> [data.[e, 0]; data.[e, 1]; data.[e, 3]])
|> array2D
|> fun a -> ws.Range("I2").Resize(40, 3).Value2 <- a
ws.Range("H1").Resize(1, 4).Value2 <- [| "班"; "番号"; "氏名"; "性別" |]
ws.Range("I1").Resize(41, 3).Borders.LineStyle <- XlLineStyle.xlContinuous
ws.Range("I1").Resize(41, 3).Borders.Weight <- XlBorderWeight.xlHairline
;;

続いて,各班の外側の罫線と班番号記入,列幅調整など実行します。

F# Interactive
seisou
|> List.map List.length
|> stackRows
|> List.iteri
       (fun i (row, n) ->
            ws.Range("H2").Offset(row, 0).Resize(n, 4) |> borderOutside;
            ws.Range("H2").Offset(row, 0).Value2 <- $"{[ '1'..'6' ].[i]}班")
ws.Range("H1:K1") |> borderOutside
ws.Range("H1:K1").EntireColumn.AutoFit()
;;

先に示したイメージで清掃班の表ができました。

月間行事予定表

この例は,個人データを使いませんので新規ワークブックでも使えます。
そろそろ新年度の行事予定が固まる時期です。
2021年度の月間行事予定表作るために月別のシートを用意してみます。

まずはワークシート作成です。

F# Interactive
[ 4..12 ] @ [ 1..3 ]
|> List.rev
|> List.iter (fun e -> (wb.Worksheets.Add() :?> _Worksheet).Name <- $"{e}月")
;;

これで4月~3月のシートができました。

次に,各シートにその月の日付と曜日を書き込みますが,まずは,日付(System.DateTime)を渡すと対応した月のシートに日付と曜日を書き込む関数を作ります。

F# Interactive
let cal (date : DateTime) =
    let y, m = date.Year, date.Month
    let dim = DateTime.DaysInMonth(y, m)    // その月の日数を得る
    let wsm = wb.Worksheets.[$"{m}月"] :?> _Worksheet    // 当該月のシートをwsmとする
    wsm.Range("A2:C2").Value2 <- [| "日"; "曜"; "行事" |]
    wsm.Range("A3").Resize(dim, 2).Value2 <-
        array2D [ for d in 1..dim -> [ box d; box $"{DateTime(y, m, d):ddd}" ] ]
    wsm.Range("A1:B1").EntireColumn.AutoFit() |> ignore
    wsm.Range("A1").Value2 <- $"2021年度 {m}月行事予定表"    // ここでは2021年度の例
    Marshal.FinalReleaseComObject(wsm) |> ignore    // COM解放
;;

関数ができたら,2021年4月~2022年3月までの毎月1日(別に1日である必要はない)を関数calに渡します。

F# Interactive
[ 0..11 ]
|> List.map (fun e -> DateTime(2021, 4, 1).AddMonths(e))
|> List.iter cal
;;

各月のシートに日付などが記入されていると思います。
年に1回の作業ですが,一度スクリプトにしておくと時間の節約になります。

その他

ちょっとした小ワザなど。

満年齢

先の個人データを使って,今年(2021年)の10月1日現在の生徒の満年齢を計算します。
Excelでも満年齢の計算は工夫が必要です。検索すると「隠し関数 DATEDIF」の使用例などが紹介されています。(Lotus 1-2-3 のサポート用らしい)
とにかく満年齢だけわかればいいのなら簡易的に8桁整数で引き算する方法で十分です。
名簿データの隣(F列)に書き込んでみます。

F# Interactive
data.[1.., 4]    // スライスによって生年月日のデータだけを抜き出す
|> Array.map (fun e -> (20211001 - int $"{e:yyyyMMdd}") / 10000)
|> Array.singleton
|> Array.transpose
|> array2D
|> fun a -> ws.Range("F2:F41").Value2 <- a
ws.Range("F1").Value2 <- "年齢"
ws.Range("F1").EntireColumn.AutoFit()
;;

別にワークシート関数でもいいですが...

学級別シートの作成

3学年それぞれA組からE組まであるものとします。
15学級分のワークシートを作ります。
まず,学級名のリストを作ります。

F# Interactive
let classroom =
    ([ 1..3 ], [ 'A'..'E' ])
    ||> List.allPairs
    |> List.map (fun (n, k) -> $"{n}年{k}組")
;;

リストから学級名のシートを作ります。

F# Interactive
classroom
|> List.rev
|> List.iter (fun e -> (wb.Worksheets.Add() :?> _Worksheet).Name <- e)
;;

最後に

お付き合いいただきありがとうございます。
作業後は「後始末」(準備編参照)をお忘れなく。

いつも,ちょっとしたスクリプトができると,Google Keepにメモしておきます。
リマインダーを設定しておくと,適切な時期に通知してくれますので,スクリプトを探さなくても済みます。

何もかもF#にする気は皆無でして,現在も状況に応じてVBAやWSHをよく利用します。
IronPythonもExcelと相性がいいので,いずれまとめておきたいと思います。

個人的な備忘録としてまとめたものですが,どなたかのお役に立てば幸いです。

参考

2012年頃,書店で偶然手にした「実践F# 関数型プログラミング入門」がF#にハマるきっかけでした。何度も読み返しましたが,最近,やっと半分くらい消化できたようです。

Excel操作は,F#のコレクションについて実践的な学習をする際にいい課題になりました。
「Scripting Excel tests with F#」が私にとってのスタートでした。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1