Home About Contact
Haskell , JavaScript , Deno

セルのアドレス生成をもっと簡単に計算したい

danfo.js 空のセルが存在するエクセルファイルを読み込み失敗するなど で SheetJSからセルの値を読み取るときに、セルアドレスを生成するためのコードで、行列を二重にループしていた。 これをもっと簡単に書きたい。

このセルアドレスを作り出す処理は、もし Haskell だったら:

Prelude> f row col = (row,col)
Prelude> pure f <*> [0..5] <*> [0..2]
[(0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2),(3,0),(3,1),(3,2),(4,0),(4,1),(4,2),(5,0),(5,1),(5,2)]

とすればよい。

JSでも、このような計算はできないのか?

fast-cartesian というライブラリがあったので使ってみます。

$ mkdir celladdress
$ cd celladdress
$ nmp init -y
$ npm install fast-cartesian
$ touch index.js

このライブラリはどうやら ES Modules 方式が必要で、commonJS の require が使えないらしいので、 package.json に以下を追記:

"type": "module",

package.json 全体:

{
  "type": "module",
  "dependencies": {
    "fast-cartesian": "^7.5.0"
  }
}

index.js を書きます。

// index.js

import fastCartesian from 'fast-cartesian';

//
// 6行x3列の範囲を読み込む.
//
const rowCount = 6
const colCount = 3

const rowRange = [...Array(rowCount).keys()];
const colRange = [...Array(colCount).keys()];

const allAddresses = fastCartesian([rowRange, colRange]);
console.log(allAddresses)

実行:

$ node index.js 
[
  [ 0, 0 ], [ 0, 1 ], [ 0, 2 ],
  [ 1, 0 ], [ 1, 1 ], [ 1, 2 ],
  [ 2, 0 ], [ 2, 1 ], [ 2, 2 ],
  [ 3, 0 ], [ 3, 1 ], [ 3, 2 ],
  [ 4, 0 ], [ 4, 1 ], [ 4, 2 ],
  [ 5, 0 ], [ 5, 1 ], [ 5, 2 ]
]

できました。

実際に前回のエクセルデータで試す

maclist-wit-empty

では、対象となる前回のエントリーのコードでこれを使おうと思ったのですが、 ライブラリのインポート方式が CommonJS と ES Modules 方式で別々となり面倒。 さらに調査したところ、 CommonJS 方式の同じ機能を持つライブラリがあった cartesian-product、これを使います。

$ npm install cartesian-product

CommonJS方式なので、package.json から以下の記述は削除します。

"type": "module",

もう面倒。Node.js をやめて Deno に移行したい。

まずは、対象となる範囲のセルアドレスを生成します。

const XLSX = require('xlsx')
const DFD = require('danfojs-node');
const product = require('cartesian-product');

//
// 6行x3列の範囲を読み込むために エクセルのアドレスを生成する.
//
const rowCount = 6
const colCount = 3

const rowRange = [...Array(rowCount).keys()];
const colRange = [...Array(colCount).keys()];

const cellAddressList = product([rowRange, colRange]).map( (rowAndCol)=> {
    const rowIndex = rowAndCol[0];
    const colIndex = rowAndCol[1];
    return XLSX.utils.encode_cell( {c:colIndex, r:rowIndex} );
});

console.log(cellAddressList);

実行:

$ node index.js
[
  'A1', 'B1', 'C1', 'A2',
  'B2', 'C2', 'A3', 'B3',
  'C3', 'A4', 'B4', 'C4',
  'A5', 'B5', 'C5', 'A6',
  'B6', 'C6'
]

うまくいきました。

では、SheetJSでエクセルデータを読み込み、danfo.js で処理する部分のコードを足して完成させます。

const XLSX = require('xlsx')
const DFD = require('danfojs-node');
const product = require('cartesian-product');

//
// 6行x3列の範囲を読み込む.
//
const rowCount = 6
const colCount = 3

const rowRange = [...Array(rowCount).keys()];
const colRange = [...Array(colCount).keys()];

const workbook = XLSX.readFile('maclist-with-empty.xlsx');
const sheetNames = workbook.SheetNames;
const worksheet = workbook.Sheets[sheetNames[0]];

const cellList = product([rowRange, colRange]).map( (rowAndCol)=> {
    const rowIndex = rowAndCol[0];
    const colIndex = rowAndCol[1];
    const cellAddress = XLSX.utils.encode_cell( {c:colIndex, r:rowIndex} );
    const cell = worksheet[cellAddress];

    if( cell && cell.w ){
        return {
            text: cell.w,
            colIndex: colIndex,
            rowIndex: rowIndex};
    }
    else {
        // 値が空のセルは空文字列を割り当てます.
        return {
            text: '',
            colIndex: colIndex,
            rowIndex: rowIndex};
    }
}).flat();

const head = (list)=>{ return list[0]; }
const tail = (list)=>{ return list.slice(1) };

const column0Values = cellList.filter((cell)=> cell.colIndex==0).map((cell)=> cell.text);
const column1Values = cellList.filter((cell)=> cell.colIndex==1).map((cell)=> cell.text);
const column2Values = cellList.filter((cell)=> cell.colIndex==2).map((cell)=> cell.text);

const tableObject = {};

const df = new DFD.DataFrame({
    [head(column0Values)]: tail(column0Values),
    [head(column1Values)]: tail(column1Values),
    [head(column2Values)]: tail(column2Values)
});

df.print()

実行:

 $ node index.js
╔════════════╤═══════════════════╤═══════════════════╤═══════════════════╗
║            │ kind              │ name              │ price             ║
╟────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 0          │ laptop            │ MacBook Air       │ 98000             ║
╟────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 1          │ laptop            │ MacBook Pro       │ 248000            ║
╟────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 2          │                   │ Mac mini          │ 78000             ║
╟────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 3          │ desktop           │ iMac              │ 154800            ║
╟────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 4          │ desktop           │ MacPro            │ 715000            ║
╚════════════╧═══════════════════╧═══════════════════╧═══════════════════╝

できました。

追伸 Deno でやってみる

Deno で npm のライブラリ使えるようになったので、試してみる。

Denoはインストールされていることが前提になるが、 プロジェクトディレクトリの作成とか初期化とか不要。 いきなりコード書いてよい。

productCellAddresses.js

import product from "npm:cartesian-product";
import XLSX from "npm:xlsx";

const rowCount = 6
const colCount = 3

const rowRange = [...Array(rowCount).keys()];
const colRange = [...Array(colCount).keys()];

const cellAddressList = product([rowRange, colRange]).map((rowAndCol)=> {
    const rowIndex = rowAndCol[0];
    const colIndex = rowAndCol[1];
    return XLSX.utils.encode_cell( {c:colIndex, r:rowIndex} );
});

console.log(cellAddressList);

ちなみに使用した Deno のバージョンはこれ:

$ deno --version
deno 1.28.3 (release, x86_64-unknown-linux-gnu)
v8 10.9.194.5
typescript 4.8.3

実行:

$ deno run productCellAddresses.js
[
  "A1", "B1", "C1", "A2",
  "B2", "C2", "A3", "B3",
  "C3", "A4", "B4", "C4",
  "A5", "B5", "C5", "A6",
  "B6", "C6"
]

できた。