Closed20

Notion API による simple table ヘルパー

nikogolinikogoli

モチベ:notion の simple table の微妙な使いづらさをなんとかしたい

スクラップ内目次

理想は chrome extension にしてボタンを押したら...って感じだけど

nikogolinikogoli

細かい前提条件の話とか

  • 型を全く付けてないので、deno で実行するときは--no-checkが必要

  • 個人使用だし、コメントも使わないので、基本的にテーブル自体の差し替えを行う

  • callout の中に操作する対象を入れておいて、それを親要素としていろいろする

  • 公式doc 内に表記ゆれ?があり、"rich_text":...になっているが retrieve API の結果は "text":...である。が、どっちを使っても問題なく動く[1]

脚注
  1. つまり、"rich_text": [ {"type": "text", "text": { "content": "hoge", "link": null } } ]の最初の rich_text を text に変えてもエラーになることはない ↩︎

nikogolinikogoli

再考

  • callout の中身を検証せずに対象要素を選んでいる。filter を使って type が table かどうかを確かめる
  • 関数化
    • ソートの実装
  • セルの中身を確定したあとで一部を変更すると、内部的には別の rich text object になる仕様らしい。例えば、『みどり1』を後から『緑1』に変更すると、セルのデータは「『緑』,『1』」になる
    • 要するに、見た目は1オブジェクトでもデータがそうである保証はないので、cells.map(cell => cell[0].plain_text)決め打ちでテキストを取得しては駄目
  • テキスト情報の保持
nikogolinikogoli

table block に対して、table row を変更するような update を適用することはできない[1]

  • 「children を持つ block を API で操作する」、つまり「親-子」の2種のブロックを一度のAPI call で操作することは許容されない[2]
  • 唯一?の例外が table block で、仕様上少なくとも1つの table row block を伴っていないと存在できない。そのため、table block を append するときのみ、「children を持つ block を API で操作する」ことが認められている
脚注
  1. childern should not be presentみたいなエラーメッセージが出る ↩︎

  2. 少なくともページやテーブルまわりではそう。データベースは知らない ↩︎

nikogolinikogoli

汎用関数 (ってほど汎用ではないが)

table row block を作るときの prop を設定するもの
function set_celldata_obj(is_eq, text){
    const annot_props = { "bold": false, "italic": false, "strikethrough": false,
        "underline": false, "code": false, "color": "default" };
    if (text) {
        let obj = new Object({"annotations": annot_props, "plain_text": text, "href": null});
        if (is_eq) {
            obj.type = "equation";
            obj.equation = {"expression": text}
        } else {
            obj.type = "text";
            obj.text = {"content": text, "link": null}
        return [obj]
    }} else {
        return []
    }
}
nikogolinikogoli

リストから simple table へ変換

異なるラベル(列名)を許容する。今のところはソートなし

  • 名前:山田、性別:男、年齢:22
  • 名字:田中、性別:女、年齢:20

名前 性別 年齢 名字
山田 22
20 田中
コード
// 構成:ページ - 親要素 - ルート要素 - リスト
// ラベル(列名)の不一致を許容する:リスト文章には存在しない列名の場合、テーブルでは空セルにする
// リストの構成:・日本語名:なにか、英語名:something、カラーコード:#090909
//               ・日本語:何でも、英語:anything、カラーコード:#000000
const target_url = "https://www.notion.so/~~ページid~~#~~親要素id~~";
const [page_id, parent_id] = target_url.match(/so\/(.+)#(.+)/).slice(1).map(x => x);
let root_id = ""; // リストの直接の親要素のid
const [sep_cell, sep_lab] = ["、",":"]; // リストにおけるセル部・ラベル部の区切り文字

// 親要素の id から、リストのルート要素を取得
const response = await notion.blocks.children.list({ block_id:parent_id }).then( async response => {
    root_id = response.results[0].id;
    // リストのデータを得る
    return await notion.blocks.children.list({ block_id:root_id })
}).then( response => {
    // 必要な部分を抜き出す
    const org_objs_list = response.results.map(b => new Object({"id":b.id, "type":b.type, "bulleted_list_item":b.bulleted_list_item }) );

    // データを列名をキーとするオブジェクトに変える + 列名の一覧を作成
    const texts_list = org_objs_list.map(item => item.bulleted_list_item.text[0].plain_text);
    const obj_list = texts_list.map(text => Object.fromEntries(text.split(sep_cell).map(cell => cell.split(sep_lab))) );
    const labels = [...new Set(obj_list.map(ob => Object.keys(ob)).flat())]

     // 各行データにおいて、重複なしのラベル名の存在を確認し、対応するセルがなければ空セルを挿入する
     const arranged_obj_list = obj_list.map( obj => {
        const copied = {...obj}
        labels.forEach(nm => (copied[nm]) ? undefined : copied[nm]="")
        return copied
    })

    // ヘッダーおよび各行のtable row block object を作成し。それらをまとめて table block object を作成
    const header_rowobjs = new Object({ "object": 'block', "type": "table_row",
        "table_row": {"cells": labels.map(text => set_celldata_obj(false, text)) }
    })
    const rowobjs_list = arranged_obj_list.map(item => new Object({
        "object": 'block', "type": "table_row",
        "table_row": {"cells": labels.map(nm => set_celldata_obj(false, item[nm]) )}
    }))
    const table_props = { "object": 'block', "type": "table", "has_children": true,
        "table": { "table_width": labels.length,
            "has_column_header": true,
            "has_row_header": false,
            "children": [header_rowobjs].concat(rowobjs_list)
        }
    }
    return Promise.resolve(table_props)
}).then(async (table_props) => {
    // 親要素にテーブルを追加
    return await notion.blocks.children.append({
        block_id:parent_id,
        children: [table_props]
    });
}).then( async () => {
    // 親要素からルート要素、つまりリスト全体を削除
    return await notion.blocks.delete({ block_id:root_id })
}).then(
    response => console.log(response)
)
nikogolinikogoli

simple table をリストに変換

上の逆

コード
// 構成:ページ - 親要素 - テーブル
//                      - ルート要素 - リスト (こっちは追加するもの)
// テーブルの構成:| 日本語名 |  英語名   | カラーコード |
//                |  なにか | something |   #090909   |
const target_url = "https://www.notion.so/~~ページid~~#~~親要素id~~";
const [page_id, parent_id] = target_url.match(/so\/(.+)#(.+)/).slice(1).map(x => x);
let table_id = "";
let root_id = ""; // 新規に追加する、リストの親要素のid
const [sep_cell, sep_lab] = ["、",":"]; // リストにおけるセル部・ラベル部の区切り文字

// 親要素の id から、テーブルを取得
const temp = await notion.blocks.children.list({ block_id:parent_id }).then( async response => {
    table_id = response.results[0].id;
    // 親要素に「リストの親」となるルート要素を追加
    const root_obj = { "object":"block", "type": "numbered_list_item", "numbered_list_item": { "rich_text": [ ], "color": "default" } };
    return await notion.blocks.children.append({
        block_id: parent_id,
        children: [root_obj]
    })
}).then(async (response) => {
    root_id = response.results[0].id; // 追加した要素の id を保存しておく
    return await notion.blocks.children.list({ block_id:table_id }) // テーブルの各行のデータを得る
}).then( response => {
    // 必要な部分を抜き出す
    const org_objs_list = response.results.map(b => new Object({"id":b.id, "type":b.type, "table_row":b.table_row }) );
    // セル内容のテキストからなる2次配列を作成
    const texts_matrix = org_objs_list.map(row => row.table_row.cells.map(cell => cell[0].plain_text));
    // ラベルを取得し、行ごとに「ラベル1:セル内容、ラベル2:セル内容、...」の文字列を作る
    const labels = texts_matrix[0];
    const texts_list = texts_matrix.slice(1).map(row => row.reduce( (pre, tx, idx) => `${pre}${labels[idx]}${sep_lab}${tx}${sep_cell}`, "").slice(0,-1));
    // 作成した文字列から bulleted list item object を作る
    const new_objs_list = texts_list.map(text => new Object(
        {   "object":"block",
            "type": "bulleted_list_item",
            "bulleted_list_item": {
                "rich_text": [ {"type": "text", "text": { "content": text, "link": null } } ],
                "color": "default",
            }
        }
    ));
    return Promise.resolve(new_objs_list)
}).then(async (new_objs_list) => {
    // 最初に追加しておいたルート要素を対象に、リストを追加
    return await notion.blocks.children.append({
        block_id: root_id,
        children: new_objs_list
    })
}).then( async () => {
    // 親要素から元のテーブルを削除
    return await notion.blocks.delete({ block_id:table_id })
}).then(
    response => console.log(response)
)
nikogolinikogoli
  • 既存のテーブルにリストから追加
  • ラベルを付けない版
nikogolinikogoli

テーブルを2つに分割

空行を境として、1つのテーブルを2つにわける

名前 性別 年齢
山田 22
田中 20

名前 性別 年齢
山田 22
名前 性別 年齢
田中 20
コード
// 構成:ページ - 親要素 - テーブル (途中に空の行あり)
// テーブルの構成:| 日本語名 |  英語名   | カラーコード |
//                 |   なにか | something |   #090909    |
//                 |      |       |        |
//                 |   なにか | something |   #090909    |
const target_url = "https://www.notion.so/~~ページのid~~#~~親要素のid~~";
const [page_id, parent_id] = target_url.match(/so\/(.+)#(.+)/).slice(1).map(x => x);
let table_id = "";
let headers = [false, false]; // ヘッダー色付けの設定を元のテーブルに合わせる
let header_row_obj = undefined; // ラベル行を元のテーブルから流用する

// 親要素から、テーブルの id と設定情報を取得
await notion.blocks.children.list({ block_id:parent_id }).then( async ({results}) => {
    table_id = results[0].id;
    headers = [results[0].table.has_column_header, results[0].table.has_row_header];
    return await notion.blocks.children.list({ block_id:table_id })
}).then( async ({results}) => {
    // 必要な部分を抜き出す
    const org_objs_list = results.map(b => {
        const {type, table_row} = b;
        return {"object":"block", type, table_row}
    });
    header_row_obj = org_objs_list[0]; // ラベル行を保存しておく

    // 切れ目とする空白行のインデックスを調べる
    const separation_idx = org_objs_list.findIndex(x => x.table_row.cells[0].length==0);
    if (separation_idx < 0) {throw new Error('区切りを示す空白行が見つかりません。再確認してください')};

    // 切れ目で table row block object のリストを2つに分ける + 後半のリストの頭にラベル行を追加する
    const first_objs_list = org_objs_list.slice(0, separation_idx);
    const second_objs_list = [header_row_obj].concat(org_objs_list.slice(separation_idx+1));

    // それぞれの object のリストで table block object を作ってリストにまとめる
    const table_objs = [first_objs_list, second_objs_list].map(lis => {
        return new Object({ "object": 'block',
            "type": "table",
            "has_children": true,
            "table": { "table_width": header_row_obj.table_row.cells.length, // ラベル行のセル数を流用
                "has_column_header": headers[0], // 元のテーブルに合わせる ↓も同じ
                "has_row_header": headers[1],
                "children": lis
            }
        })
    });

    // ルート要素に2つのテーブルを追加
    return await notion.blocks.children.append({
        block_id: parent_id,
        children: table_objs
    })
}).then( async () => {
    // 親要素から元のテーブルを削除
    return await notion.blocks.delete({ block_id:table_id })
}).then(
    response => console.log(response)
)
nikogolinikogoli

複数のテーブルを1つに結合

上の逆。異なる列名を許容する。今のところはソートなし

名前 性別 年齢
山田 22
名字 性別 年齢
田中 20

名前 性別 年齢 名字
山田 22
20 田中
コード

// 構成:ページ - 親要素 - 複数のテーブル (列名に異なるものあり)
// 列名の不一致を許容する:元のテーブルには存在しない列名の場合、結合後のテーブルでは空セルにする
// テーブルの構成:    その1                   その2


const target_url = "https://www.notion.so/~~ページid~~#~~親要素id~~";
const [page_id, parent_id] = target_url.match(/so\/(.+)#(.+)/).slice(1).map(x => x);
let results_list = []; // 複数の API call の結果を格納する
let table_ids = [];
let headers_list = []; // 元のテーブルのヘッダー色付けの設定を格納する
let header_names = []; // 元のテーブルのラベル行のテキストを格納する


await notion.blocks.children.list({ block_id:parent_id }).then( async ({results}) => {
    // 親要素以下の table block object の id と ヘッダーの設定を取得する
    results.filter(item => item.type=="table").map( ({id, table}) => {
        table_ids.push(id);
        headers_list.push([table.has_column_header, table.has_row_header])
    });

    // 各 id について行データを取得する
    return await table_ids.reduce((promise, id) => {
        return promise.then(async () => {
            await notion.blocks.children.list({ block_id:id }).then( ({results}) => {results_list.push(results)} )
        })
    }, Promise.resolve() )
}).then( async () => {
    // 各行データをラベル名をキーとするオブジェクトに変更する + 重複なしのラベル名の配列を作成
    const rowobj_list = results_list.map(res => {
        const labels = res[0].table_row.cells.map(cell => cell[0].plain_text)
        header_names.push(labels)
        return res.slice(1).map(b => {
            let obj = new Object()
            b.table_row.cells.forEach((cell, idx) => obj[labels[idx]] = cell)
            return obj
        });
    }).flat()
    header_names = [...new Set(header_names.flat())]
    
    // 各行データにおいて、重複なしのラベル名の存在を確認し、対応するセルがなければ空セルを挿入する
    const arranged_rowobj_list = rowobj_list.map( rowobj => {
        const copied = {...rowobj}
        header_names.forEach(nm => (copied[nm]) ? undefined : copied[nm]=[])
        return copied
    })

    // 行データから table row blcok object を作成
    const header_row_obj = new Object({
        "object": 'block', "type": "table_row",
        "table_row": { "cells": header_names.map(nm => set_celldata_obj(false, nm)) }
    })
    const row_objs_list = arranged_rowobj_list.map(row => new Object({
        "object": 'block', "type": "table_row",
        "table_row": { "cells": header_names.map(nm => row[nm]) }
    }) )
    const table_rows = [header_row_obj].concat(row_objs_list);

    // 全ての行データをまとめた1つの table block object を作成
    const table_objs = new Object({ "object": 'block',
        "type": "table",
        "has_children": true,
        "table": { "table_width": header_names.length,
            "has_column_header": headers_list[0][0], // 元のテーブルの1つ目に合わせる ↓も同じ
            "has_row_header": headers_list[0][1],
            "children": table_rows
        }
    })
    // ルート要素にテーブルを追加
    return await notion.blocks.children.append({
        block_id: parent_id,
        children: [table_objs]
    })
}).then( async () => {
    // 親要素から元のテーブルを削除
    return await table_ids.reduce((promise, id) => {
        return promise.then(async () => {
            await notion.blocks.delete({ block_id:id }).then( responce => console.log(responce) )
        })
    }, Promise.resolve() )
}).then(
    response => console.log(response)
)

nikogolinikogoli

列を指定してソート

1重だけ

コード
// 構成:ページ - 親要素 - テーブル
// 列番号を指定してソートが可能
// テーブルの構成:  | 日本語名 |  英語名   | カラーコード |
//                 |   なにか | something |   #090909   |

const target_url = "https://www.notion.so/~~ページのid~~#~~親要素のid~~";
const [page_id, parent_id] = target_url.match(/so\/(.+)#(.+)/).slice(1).map(x => x);
let table_id = "";
let headers = [false, false]; // ヘッダー色付けの設定を元のテーブルに合わせる
let table_width = 0; 

// ソート指定:基準列(-1 ならソートなし)、降順にするかどうか、数値として評価するかどうか
const use_sort = {"index": 1, "reverse": false, "as_int": false}

await notion.blocks.children.list({ block_id:parent_id }).then( async ({results}) => {
    // 親要素以下の table block object の id と ヘッダーの設定と元のテーブルの列数を取得する
    const {id, table } = results.find(item => item.type=="table")
    table_id = id;
    headers = [table.has_column_header, table.has_row_header]
    table_width = table.table_width

    // テーブルの行データを取得
    return await notion.blocks.children.list({ block_id:table_id })
}).then( ({results}) => {
    // 行データから必要な情報を取り出す
    const org_rowobjs_list = results.map(item => {
        const {type, table_row} = item;
        return {type, table_row}
    })

    // ソート処理
    let new_rowobjs_list = []
    if (use_sort.index < 0) {
        // ソートなし
        new_rowobjs_list = [...org_rowobjs_list]
    } else {
        // ソートあり 指定列の存在をチェック
        if (use_sort.index > table_width) {throw new Error("ソート基準に指定した列の番号がテーブルの列数を超過しています")}

        // 各行の指定した列のセルのテキストを取得し、元の行番号と一緒にまとめる
        const target_text_objs = org_rowobjs_list.map( ({"table_row":{"cells": cells}}, idx) => {
            const text = (cells[use_sort.index-1].length) ? cells[use_sort.index-1][0].plain_text : "";
            return {text, idx}
        }).slice(1) // ラベル行は飛ばす

        // ソートと必要なら逆順化
        let sorted_objs = []
        if (!use_sort.as_int) {
            sorted_objs = [...target_text_objs].sort((a,b) => (a.text < b.text) ? -1 : 1)
        } else {
            sorted_objs = [...target_text_objs].sort((a,b) => Number(a.text) -Number(b.text))
        }
        if (use_sort.reverse) {sorted_objs.reverse()}

        // ソートされた行番号の順に行を呼ぶことで、行データを並び替える
        new_rowobjs_list = [org_rowobjs_list[0]].concat(sorted_objs.map( ({idx}) => org_rowobjs_list[idx] ))
    }

    // 更新した行データから、table block object を作成する
    const table_props = { "object": 'block', "type": "table", "has_children": true,
        "table": { "table_width": table_width,
            "has_column_header": true,
            "has_row_header": false,
            "children": new_rowobjs_list
        }
    }
    return Promise.resolve(table_props)
}).then(async (table_props) => {
    // 親要素にテーブルを追加
    return await notion.blocks.children.append({
        block_id:parent_id,
        children: [table_props]
    });
}).then( async () => {
    // 親要素から元のテーブルを削除
    return await notion.blocks.delete({ block_id:table_id })
}).then(
    response => console.log(response)
)
nikogolinikogoli

各行の行頭に連番を振る

列を指定してソートが可能

名前 性別 年齢
山田 22
田中 20

名前 性別 年齢
1 山田 22
2 田中 20
コード

ソートのコードに番号追加処理を加えただけ

// 構成:ページ - 親要素 - テーブル
// 列番号を指定してソートが可能
// テーブルの構成:  | 日本語名 |  英語名   | カラーコード |
//                 |   なにか | something |   #090909    |

const target_url = "https://www.notion.so/~~ページid~~#~~親要素id~~";
const [page_id, parent_id] = target_url.match(/so\/(.+)#(.+)/).slice(1).map(x => x);
let table_id = "";
let headers = [false, false]; // ヘッダー色付けの設定を元のテーブルに合わせる
let table_width = 0; 

// ソート指定:基準列(-1 ならソートなし)、降順にするかどうか、数値として評価するかどうか
const use_sort = {"index": -1, "reverse": false, "as_int": false}

await notion.blocks.children.list({ block_id:parent_id }).then( async ({results}) => {
    // 親要素以下の table block object の id と ヘッダーの設定と元のテーブルの列数を取得する
    const {id, table } = results.find(item => item.type=="table")
    table_id = id;
    headers = [table.has_column_header, table.has_row_header]
    table_width = table.table_width

    // テーブルの行データを取得
    return await notion.blocks.children.list({ block_id:table_id })
}).then( ({results}) => {
    // 行データから必要な情報を取り出す
    const org_rowobjs_list = results.map(item => {
        const {type, table_row} = item;
        return {type, table_row}
    })

    // ソート処理
    let new_rowobjs_list = []
    if (use_sort.index < 0) {
        // ソートなし
        new_rowobjs_list = [...org_rowobjs_list]
    } else {
        // ソートあり 指定列の存在をチェック
        if (use_sort.index > table_width) {throw new Error("ソート基準に指定した列の番号がテーブルの列数を超過しています")}

        // 各行の指定した列のセルのテキストを取得し、元の行番号と一緒にまとめる
        const target_text_objs = org_rowobjs_list.map( ({"table_row":{"cells": cells}}, idx) => {
            const text = (cells[use_sort.index-1].lenght) ? cells[use_sort.index-1][0].plain_text : "";
            return {text, idx}
        }).slice(1) // ラベル行は飛ばす

        // ソートと必要なら逆順化
        let sorted_objs = []
        if (!use_sort.as_int) {
            sorted_objs = [...target_text_objs].sort((a,b) => (a.text < b.text) ? -1 : 1)
        } else {
            sorted_objs = [...target_text_objs].sort((a,b) => Number(a.text) -Number(b.text))
        }
        if (use_sort.reverse) {sorted_objs.reverse()}

        // ソートされた行番号の順に行を呼ぶことで、行データを並び替える
        new_rowobjs_list = [org_rowobjs_list[0]].concat(sorted_objs.map( ({idx}) => org_rowobjs_list[idx] ))
    }

    // 先頭のラベル行には空セル、それ以外には連番セルを、セルのリストの先頭に追加する
    const table_rows = new_rowobjs_list.map( ({type, table_row}, idx) => {
        let copied = {"object": "block", "type": type}
        if (idx==0) {
            const new_cells = [set_celldata_obj(false, "")].concat(table_row.cells);
            copied["table_row"] ={ "cells": new_cells }
        } else { const new_cells = [set_celldata_obj(false, String(idx))].concat(table_row.cells); copied["table_row"] = {"cells": new_cells} }
        return copied    });

    // 更新した行データから、table block object を作成する
    const table_props = { "object": 'block', "type": "table", "has_children": true,
        "table": { "table_width": table_width +1,
            "has_column_header": true,
            "has_row_header": false,
            "children": table_rows
        }
    }
    return Promise.resolve(table_props)
}).then(async (table_props) => {
    // 親要素にテーブルを追加
    return await notion.blocks.children.append({
        block_id:parent_id,
        children: [table_props]
    });
}).then( async () => {
    // 親要素から元のテーブルを削除
    return await notion.blocks.delete({ block_id:table_id })
}).then(
    response => console.log(response)
)
nikogolinikogoli

テーブルを転置する (行と列を入れ替える)

マークダウンでは表示できないが、行ヘッダーと列ヘッダーの入れ替えも行われる

名前 性別 年齢
山田 22
田中 20

名前 山田 田中
性別
年齢 22 20
コード
// 構成:ページ - 親要素 - テーブル
// テーブルの構成:  | 日本語名 |  英語名   | カラーコード |
//                 |   なにか | something |   #090909   |

const target_url = "https://www.notion.so/~~ページid~~#~~親要素id~~";
const [page_id, parent_id] = target_url.match(/so\/(.+)#(.+)/).slice(1).map(x => x);
let table_id = "";
let headers = [false, false]; // ヘッダー色付けの設定
let table_width = 0; 


await notion.blocks.children.list({ block_id:parent_id }).then( async ({results}) => {
    // 親要素以下の table block object の id と ヘッダーの設定と元のテーブルの列数を取得する
    const {id, table } = results.find(item => item.type=="table")
    table_id = id;
    headers = [table.has_column_header, table.has_row_header]
    table_width = table.table_width

    // テーブルの行データを取得
    return await notion.blocks.children.list({ block_id:table_id })
}).then( ({results}) => {
    // 行データから必要な情報を取り出す
    const org_rowobjs_list = results.map(item => {
        const {type, table_row} = item;
        return {type, table_row}
    })

    // セルデータの2次元配列を作り、(手動で)転置する
    const org_cell_matrix = org_rowobjs_list.map( ({"table_row":{"cells":cells}}) => cells)
    const new_cell_matrix = [...Array(table_width)].map( (x,idx) => org_cell_matrix.map( cells => cells[idx]) )

    // 転置したデータを使って table row block object を作成
    const row_objs_list = new_cell_matrix.map(row => new Object({
        "object": 'block', "type": "table_row", "table_row": { "cells": row }
    }) )

    // 更新した行データから、table block object を作成する
    const table_props = { "object": 'block', "type": "table", "has_children": true,
        "table": { "table_width": new_cell_matrix[0].length, //転置後の行列から列数を取得する
            "has_column_header": headers[1], // 元のテーブルとは設定を入れ替える ↓も同じ
            "has_row_header": headers[0],
            "children": row_objs_list
        }
    }
    return Promise.resolve(table_props)
}).then(async (table_props) => {
    // 親要素にテーブルを追加
    return await notion.blocks.children.append({
        block_id:parent_id,
        children: [table_props]
    });
}).then( async () => {
    // 親要素から元のテーブルを削除
    return await notion.blocks.delete({ block_id:table_id })
}).then(
    response => console.log(response)
)
nikogolinikogoli

計算系 (とりあえず)

  • 合計 (SUM)、最大 (MAX)、最小 (MIN)、平均 (AVERAGE)、数え上げ (COUNT)
  • 2番目に大きい (SECONDMAX)、2番目に小さい (SECONDMIN)
  • 最大値や最小値を持つ行や列ラベルを取得 (MAXNAME、MINNAME)
  • セルを指定しその値を使って四則演算
コード
// 構成:ページ - 親要素 - テーブル
// 命令の形式:「=R_SUM(2,6)」のように、「=」+「方向指定の1文字(行R/列C)」+「命令文」+「開始セルの番号と終了セルの番号」で構成
//             例:=R_SUM(2,6) → 現在の行(R)において、2列目のセルから6列目のセルまでの合計
//                 =R_SUM()    → 範囲を省略すると、ヘッダーを除く一番左(列の場合は一番上)から当該セルの1つ手前までを対象にする
//
// 命令の種類:SUM()、MAX()、MIN()、AVERAGE()、COUNT()
//             2番目に大きい/小さい・・・SECONDMAX()、SECONDMIN()
//             最大値の行や列のラベルを表示・・・MAXNAME()、MINNAME()
//             「方向指定+セル番号」でセルを指定して四則演算・・・C2/C3+1 など
//
// 注意:第1行の左端 → 第1行の右端 → 第2行の左端 → 第2行の右端 → ... と処理する仕様なので、自分よりも右下方向のセルを
//       対象にした命令は使用できない

const target_url = "https://www.notion.so/~~ページのid~~#~~親要素のid~~";
const [page_id, parent_id] = target_url.match(/so\/(.+)#(.+)/).slice(1);
let table_id = "";
let headers = [false, false]; // ヘッダー色付けの設定
let table_width = 0; 

await notion.blocks.children.list({ block_id:parent_id }).then( async ({results}) => {
    // 親要素以下の table block object の id と ヘッダーの設定と元のテーブルの列数を取得する
    const {id, table } = results.find(item => item.type=="table")
    table_id = id;
    headers = [table.has_column_header, table.has_row_header]
    table_width = table.table_width

    // テーブルの行データを取得
    return await notion.blocks.children.list({ block_id:table_id })
}).then( ({results}) => {
    // 行データから必要な情報を取り出す
    const org_rowobjs_list = results.map(item => {
        const {type, table_row} = item;
        return {type, table_row}
    })

    // セルデータから、データの行列とテキストの行列を作成する
    const org_cell_matrix = org_rowobjs_list.map( ({"table_row":{"cells":cells}}) => cells)
    const org_text_matrix = org_cell_matrix.map( row => row.map(
        cell => (cell.length) ? cell.map( ({plain_text}) => plain_text).join("") : "" )
    )
    
    // 範囲を省略した命令で使用するための、デフォルト開始セルをヘッダーの有無に合わせて設定
    let [default_colidx, default_rowidx] = [0,0]
    if (headers[0]) {default_colidx = 1}
    if (headers[1]) {default_rowidx = 1}

    // テキスト行列のコピーを作成 → コピー行列を対象にして各セルの命令を実行し、コピー行列のセルを書き換える
    let new_text_matrix = org_text_matrix.map(r => r.map(c => c))
    org_text_matrix.forEach( (row, r_idx) => {
        row.forEach( (text, c_idx) => {

            // 命令の処理
            if (text.startsWith("=")) {
                // 命令文のパース:方向指定文字(C/R)、命令、範囲を示す数字(あるいは空文字列)を取り出す
                const matched = text.match(/=([CR])_([^\d\s]+)\((.*)\)/)
                if (matched) {
                    let target = []
                    const [direct, formula, idxs] = matched.slice(1)

                    // 方向指定と範囲指定に従い計算対象のセルを取得 範囲指定が空文字列なら、デフォルト位置から現在位置の手前まで
                    if (direct=="R" && idxs=="") {
                        target = new_text_matrix[r_idx].slice(default_colidx, c_idx)
                    } else if (direct=="R" && idxs!="") {
                        const [start, end] = idxs.split(",").map(tx => Number(tx))
                        target = new_text_matrix[r_idx].slice(start-1, end)
                    } else if (direct=="C" && idxs=="") {
                        target = new_text_matrix.slice(default_rowidx, r_idx).map(row => row[c_idx])
                    } else if (direct=="C" && idxs!="") {
                        const [start, end] = idxs.split(",").map(tx => Number(tx))
                        target = new_text_matrix.slice(start-1, end).map(row => row[c_idx])
                    }

                    // 命令文に従って計算する
                    if (formula=="SUM") { new_text_matrix[r_idx][c_idx] = target.reduce( (pre, now) => { return pre+Number(now)}, 0 ) }
                    else if (formula=="MAX") {new_text_matrix[r_idx][c_idx] = target.map(x => Number(x)).sort((a,b) => b-a)[0]}
                    else if (formula=="SECONDMAX") {new_text_matrix[r_idx][c_idx] = target.map(x => Number(x)).sort((a,b) => b-a)[1]}
                    else if (formula=="MIN") {new_text_matrix[r_idx][c_idx] = target.map(x => Number(x)).sort((a,b) => a-b)[0]}
                    else if (formula=="SECONDMIN") {new_text_matrix[r_idx][c_idx] = target.map(x => Number(x)).sort((a,b) => a-b)[1]}
                    else if (formula=="AVERAGE"){ new_text_matrix[r_idx][c_idx] = target.reduce( (pre, now) => { return pre+Number(now)}, 0 )/target.length }
                    else if (formula=="COUNT") {new_text_matrix[r_idx][c_idx] = target.length}
                    // 最大や最小を計算 → 最大値や最小値のインデックスを取得 → 先頭行/列からラベルを取得 (デフォルトを使っていると範囲が-1されているのでそれを修正)
                    else if (formula=="MAXNAME") {
                        const target_idx = target.findIndex(y => y==String( Math.max( ...target.map(x=>Number(x)) ) ))
                        if (direct=="R") {
                            new_text_matrix[r_idx][c_idx] = org_text_matrix[0][target_idx+default_colidx]
                        } else {
                            new_text_matrix[r_idx][c_idx] = org_text_matrix[target_idx+default_rowidx][0]
                        }
                    } else if (formula=="MINNAME"){
                        const target_idx = target.findIndex(y => y==String( Math.min( ...target.map(x=>Number(x)) ) ))
                        if (direct=="R") {
                            new_text_matrix[r_idx][c_idx] = org_text_matrix[0][target_idx+default_colidx]
                        } else {
                            new_text_matrix[r_idx][c_idx] = org_text_matrix[target_idx+default_rowidx][0]
                        }                        
                    } 
                } else {
                    // 命令なのにパースが失敗するとき = 命令文なしのセル指定があるとき
                    let settled = text.slice(1)

                    // セル指定( R2 や C11 など)を取り出し、指定しているセルの内容に置き換える
                    settled.match(/[RC]\d+/g).forEach(cellnum => {
                        if (cellnum[0]=="R") {
                            settled = settled.replace(cellnum, new_text_matrix[Number(cellnum.slice(1))-1][c_idx])
                        } else {
                            settled = settled.replace(cellnum, new_text_matrix[r_idx][Number(cellnum.slice(1))-1])
                        }
                    })
                    // 四則演算なら eval してその結果を挿入する 四則演算にとどまらないなら eval せずにエラーメッセージを挿入
                    if (!settled.match(/[^\d\+\-\*/\(\)\.]/g)) {new_text_matrix[r_idx][c_idx] = eval(settled).toFixed(2) } else {new_text_matrix[r_idx][c_idx] = "不適切な数式"}
                }
            } else {
                // 命令がないときはそのまま (本来は何もしなくて良い)
                new_text_matrix[r_idx][c_idx] = text 
            }
        })
    })

    // 計算後の行列から、各行のtable row block object を作成し。それらをまとめて table block object を作成
    const rowobjs_list = new_text_matrix.map(row => new Object({
        "object": 'block', "type": "table_row",
        "table_row": {"cells": row.map(text => set_celldata_obj(false, String(text)) )}
    }))

    // 更新した行データから、table block object を作成する
    const table_props = { "object": 'block', "type": "table", "has_children": true,
        "table": { "table_width": new_text_matrix[0].length, //計算後の行列から列数を取得する
            "has_column_header": headers[0], 
            "has_row_header": headers[1],
            "children": rowobjs_list
        }
    }
    return Promise.resolve(table_props)
}).then(async (table_props) => {
    // 親要素にテーブルを追加
    return await notion.blocks.children.append({
        block_id:parent_id,
        children: [table_props]
    });
}).then( async () => {
    // 親要素から元のテーブルを削除
    return await notion.blocks.delete({ block_id:table_id })
}).then(
    response => console.log(response)
)

nikogolinikogoli

最大値・最小値のセルのテキストに色をつける

方向(行/列)を指定して、その中の最大値や最小値に指定した色をつける

  • notion上では行や列の背景色を変更できるが、この機能は API としては提供されていない

コード
// 構成:ページ - 親要素 - テーブル
// 行あるいは列を対象にして、最大値・最小値のセルのテキストに色を付ける
// テーブルの構成: |     | HP | こうげき | ぼうぎょ | 
//                  | --- | --- | --- | --- |
//                  | 緑  |  45 |  49 |  49 |
//                  | 赤  |  39 |  52 |  43 |
//                  | 青  |  44 |  48 |  65 |

const target_url = "https://www.notion.so/~~ページid~~#~~親要素id~~";
const [page_id, parent_id] = target_url.match(/so\/(.+)#(.+)/).slice(1);
let table_id = "";
let headers = [false, false]; // ヘッダー色付けの設定
let table_width = 0;

// 着色の命令:方向指定(行 or 列)、最大値の色、最小値の色
let CALL = {"direct":"R", "max_color":"red", "min_color":"blue"}


await notion.blocks.children.list({ block_id:parent_id }).then( async ({results}) => {
    // 親要素以下の table block object の id と ヘッダーの設定と元のテーブルの列数を取得する
    const {id, table } = results.find(item => item.type=="table")
    table_id = id;
    headers = [table.has_column_header, table.has_row_header]
    table_width = table.table_width

    // テーブルの行データを取得
    return await notion.blocks.children.list({ block_id:table_id })
}).then( ({results}) => {
    // 行データから必要な情報を取り出す
    const org_rowobjs_list = results.map(item => {
        const {type, table_row} = item;
        return {type, table_row}
    })

    // 行データのリストから、セルオブジェクト・インデックス・テキストの情報を持つオブジェクトの2次配列を作る
    const cell_matrix = org_rowobjs_list.map(
        ( {"table_row":{"cells":cells}}, r_idx ) => cells.map(
            (cell, c_idx) => {
                const text = (cell.length) ? cell.map( ({plain_text}) => plain_text).join("") : ""
                return {cell, r_idx, c_idx, text}
            }
        )
    )
    
    // 比較範囲からラベルを排除するため、デフォルト開始セルをヘッダーの有無に合わせて設定
    let [default_colidx, default_rowidx] = [0,0]
    if (headers[0]) {default_colidx = 1}
    if (headers[1]) {default_rowidx = 1}

    // 最大・最小を評価するセルのまとまりを、ラベル行・列を排除しつつ作成する
    let targets_list = [ [{"cell":[{}], "r_idx":-1, "c_idx":-1, "text":""}] ]
    if (CALL.direct=="R") {
        targets_list = cell_matrix.slice(default_rowidx).map(row => row.slice(default_colidx))
    } else {
        targets_list = [...Array(table_width)].map( (x, idx) => cell_matrix.slice(default_rowidx).map(row => row[idx])).slice(default_colidx)
    }
    
    // 色付け
    // 最大値の処理
    if (CALL.max_color!=""){
        targets_list.forEach( (targets) => {
            // 評価対象のセルを並び替え、1番目のテキストを取得し、それと値が等しいセルを取得する (最大値のセルが複数の場合に対応)
            const first = targets.sort((a,b) => Number(b.text)-Number(a.text))[0].text
            const target_cells = targets.filter(item => item.text==first)
            // 最大値の各セルにおいて、セルの text object の annotation の色設定を変更
            target_cells.forEach(cell => {
                cell.cell.forEach(obj => obj.annotations.color=CALL.max_color)
            })
        })
    }
    // 最小値の処理: 並べ替えの方法が異なるだけ
    if (CALL.min_color!=""){
        targets_list.forEach( (targets) => {
            const first = targets.sort((a,b) => Number(a.text)-Number(b.text))[0].text
            const target_cells = targets.filter(item => item.text==first)
            target_cells.forEach(cell => {
                cell.cell.forEach(obj => obj.annotations.color=CALL.min_color)
            })
        })
    }

    // 各行のtable row block object を再構築し、それらをまとめて table block object を作成
    const rowobjs_list = cell_matrix.map(row => new Object({
        "object": 'block', "type": "table_row", "table_row": {"cells": row.map( ({cell}) => cell)}
    }))

    // 更新した行データから、table block object を作成する
    const table_props = { "object": 'block', "type": "table", "has_children": true,
        "table": { "table_width": table_width,
            "has_column_header": headers[0], 
            "has_row_header": headers[1],
            "children": rowobjs_list
        }
    }
    return Promise.resolve(table_props)
}).then(async (table_props) => {
    // 親要素にテーブルを追加
    return await notion.blocks.children.append({
        block_id: parent_id,
        children: [table_props]
    });
//}).then( async () => {
    // 親要素から元のテーブルを削除
//    return await notion.blocks.delete({ block_id:table_id })
}).then(
    response => console.log(response)
)
このスクラップは2022/03/20にクローズされました