Closed20
Notion API による simple table ヘルパー
ピン留めされたアイテム
もろもろ整理して --no-check も外せたので、ちゃんと形にしてみる
モチベ:notion の simple table の微妙な使いづらさをなんとかしたい
スクラップ内目次
-
特定の計算系の命令結果を示す列・行を追加 (計算系の簡易版みたいな感じ)
\\
\\
理想は chrome extension にしてボタンを押したら...って感じだけど
細かい前提条件の話とか
-
型を全く付けてないので、deno で実行するときは
--no-check
が必要 -
個人使用だし、コメントも使わないので、基本的にテーブル自体の差し替えを行う
-
callout の中に操作する対象を入れておいて、それを親要素としていろいろする
-
公式doc 内に表記ゆれ?があり、
"rich_text":...
になっているが retrieve API の結果は"text":...
である。が、どっちを使っても問題なく動く[1]
-
つまり、
"rich_text": [ {"type": "text", "text": { "content": "hoge", "link": null } } ]
の最初の rich_text を text に変えてもエラーになることはない ↩︎
再考
- callout の中身を検証せずに対象要素を選んでいる。filter を使って type が table かどうかを確かめる
- 関数化
- ソートの実装
- セルの中身を確定したあとで一部を変更すると、内部的には別の rich text object になる仕様らしい。例えば、『みどり1』を後から『緑1』に変更すると、セルのデータは「『緑』,『1』」になる
- 要するに、見た目は1オブジェクトでもデータがそうである保証はないので、
cells.map(cell => cell[0].plain_text)
決め打ちでテキストを取得しては駄目
- 要するに、見た目は1オブジェクトでもデータがそうである保証はないので、
- テキスト情報の保持
汎用関数 (ってほど汎用ではないが)
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 []
}
}
リストから 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)
)
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)
)
- 既存のテーブルにリストから追加
- ラベルを付けない版
テーブルを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)
)
複数のテーブルを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)
)
左右の結合?
列を指定してソート
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)
)
各行の行頭に連番を振る
列を指定してソートが可能
名前 | 性別 | 年齢 |
---|---|---|
山田 | 男 | 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)
)
テーブルを転置する (行と列を入れ替える)
マークダウンでは表示できないが、行ヘッダーと列ヘッダーの入れ替えも行われる
名前 | 性別 | 年齢 |
---|---|---|
山田 | 男 | 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)
)
除外する行・列の指定?
計算系 (とりあえず)
- 合計 (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)
)
最大値・最小値のセルのテキストに色をつける
方向(行/列)を指定して、その中の最大値や最小値に指定した色をつける
- 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にクローズされました