ReoGrid ReoGrid Web

「令和元年」と「▲1,234」を正しく出す ― Excel書式コードで作る和暦・会計表示

· unvell team
「令和元年」と「▲1,234」を正しく出す ― Excel書式コードで作る和暦・会計表示

日付は文字列ではありませんし、日本ではそもそも暦がひとつではありません。省庁・銀行・税務署へ出す書類には 和暦 が求められ、しかも元号の最初の年は「1年」ではなく「元年」と書きます。ここを間違えると書類は差し戻されます。

厄介なのは、これがビューレイヤーで一度片付ければ済む描画の問題ではない、という点です。スプレッドシートでは は数値、表示 は書式コードであり、その書式コードはセルにくっついたまま画面・印刷・PDF を通り抜け、経理から届いた .xlsx からも一緒に入ってきます。この記事はその層の話です ― Excel の表示形式は何を表現できるのか、そして ReoGrid Web はそれをどこまで再現しているのか。

表示形式は無料版の Lite で使えます。この記事の内容にライセンスキーは必要ありません。


Intl.DateTimeFormat ではだめなのか

元号入りの文字列はプラットフォーム標準でも取れます。UI のラベル用途ならそれが正解であることも多いです。

const d = new Date(2019, 6, 1); // 2019-07-01、令和の初日

new Intl.DateTimeFormat('ja-JP-u-ca-japanese', { dateStyle: 'long' }).format(d);
// → '令和元年7月1日'   ✅

new Intl.DateTimeFormat('ja-JP-u-ca-japanese', {
  era: 'long', year: 'numeric', month: 'numeric', day: 'numeric',
}).format(d);
// → '令和1/7/1'        ⚠️ 「元」が消え、「年月日」も消える

2番目の結果が問題の本質をよく表しています。区切り文字も、句読点も、そして どのオプションの組み合わせなら「元」が出るのか も、Intl 側が決めます。こちらはレイアウトを指定しているのではなく、フォーマッターと交渉しているわけです。

書式コードはその逆です。出したい出力をそのまま1つの文字列で書き、それはレンダリング経路ではなく セル に属します。

worksheet.cell('B2').setFormat('[$-ja-JP-x-gannen]ggge"年"m"月"d"日"');
// 2019-07-01 → 令和元年7月1日
// 2024-12-10 → 令和6年12月10日

同じセル、同じ書式で、どの日付でも成立します。しかもこの文字列は Excel 自身が保存しているものと同じなので、経理が作った帳票は書式を保ったまま読み込まれます。


モデル ― 値はひとつ、表示は自由

セルは値を持ち、書式コードはその値の 描かれ方 を決めます。両者が混ざることはありません ― 日付の列をソートすれば、見えているテキストではなく内部のシリアル値で並びます。

import { createReogrid, NumberFormat } from '@reogrid/lite';

const grid = createReogrid({ workspace: '#grid' });
const ws = grid.worksheet;

// セル単位
ws.cell('B2').setValue('4999').setFormat('¥#,##0');

// 範囲単位
ws.range('B2:B20').setFormat(NumberFormat.currency('¥', 0));

// プロパティとしても読み書きできる
ws.cell('C3').format = '0.00%';
ws.cell('C3').format;               // → '0.00%'
ws.cell('C3').format = undefined;   // 解除

行・列インデックス版もあります。ループの中で使うのはこちらです。

ws.setCellInput(row, col, '45636');
ws.setCellNumberFormat(row, col, 'ggge"年"m"月"d"日"');

ws.getCellNumberFormat(row, col);    // → 書式コード
ws.clearCellNumberFormat(row, col);  // → General に戻す
ws.getDisplayText(row, col);         // → '令和6年12月10日'

getDisplayText は描画・印刷・PDF 出力が内部で使っているのと同じ呼び出しです。ここが返した文字列が、そのまま紙に乗ります。


そもそも日付をどうセルに入れるか

Excel は日付を シリアル値 で保持します(1 が 1900-01-01)。ReoGrid Web も同じです。入れ方は2通り。

日付っぽい文字列を入れる。 自動的に判定して変換されます。

ws.setCellInput(2, 1, '2024/12/10');
// 45636 として格納され、書式に 'yyyy/m/d' が設定される

認識されるパターンは yyyy/m/dyyyy-m-dm/d/yyyy、および前2つに h:mm / h:mm:ss を付けた形です。

シリアル値を直接入れる。 インポートや日付ライブラリから数値で来る場合はこちらです。

ws.setCellInput(2, 1, '45636');
ws.cell(2, 1).setFormat('ggge"年"m"月"d"日"');

順序の落とし穴

自動判定は、そのセルに すでに表示形式が設定されている場合はあえてスキップ されます。書式を当てた結果ユーザーの入力が勝手に書き換わるのを防ぐためです。つまり順序に意味があります。

// ✅ 値が先、書式が後
ws.setCellInput(2, 1, '2024/12/10');            // → 45636(本物の日付)
ws.cell(2, 1).setFormat('ggge"年"m"月"d"日"');  // → 令和6年12月10日

// ❌ 書式が先
ws.cell(3, 1).setFormat('ggge"年"m"月"d"日"');
ws.setCellInput(3, 1, '2024/12/10');            // 文字列 '2024/12/10' のまま

後者ではセルが数値にならないため、日付書式が処理する対象がそもそも存在しません。あらかじめ書式を設定したテンプレートにデータを流し込むときは、日付風の文字列ではなく シリアル値を書き込む ようにすれば、この曖昧さは消えます。


元号トークン

トークンは2系統。通常の日付トークンと自由に組み合わせられます。

トークン意味2024-12-102019-07-011989-01-07
g元号の頭文字RRS
gg元号の略称
ggg元号の正式名令和令和昭和
e元号年6164
ee元号年(ゼロ埋め)060164

組み合わせるとこうなります。

ws.cell('B2').setFormat('ggge"年"m"月"d"日"');       // 令和6年12月10日
ws.cell('B3').setFormat('ge/m/d');                     // R6/12/10
ws.cell('B4').setFormat('gge.m.d');                    // 令6.12.10
ws.cell('B5').setFormat('gggee"年"mm"月"dd"日"');    // 令和06年12月10日
ws.cell('B6').setFormat('ggge"年度"');                // 令和6年度

ダブルクォートで囲んだ部分はリテラルです。"年" と書けば「年」がそのまま出ます。ここは重要で、囲まずに書いた d はラベルではなく日付トークンとして解釈されてしまいます。

元号の境界は「年」ではなく「日」

元号は改元日そのもので切り替わります。1989年1月上旬は昭和64年、その1週間後は平成元年です。

元号開始日補足
令和2019-05-01
平成1989-01-081989-01-07 はまだ昭和64年
昭和1926-12-25
大正1912-07-30
明治1868-01-25

明治より前の日付には該当する元号がありません。ge は何も出力せず、書式のリテラル部分だけが残ります。そこまで遡るデータを扱うなら、条件付きセクションで振り分けるか、西暦書式にフォールバックしてください。


元年 ― この記事の主題

元号の最初の年は「元年」と表記します。Excel はこれをロケールタグで表現し、ReoGrid Web も同じタグを解釈します。

ws.cell('B2').setFormat('[$-ja-JP-x-gannen]ggge"年"m"月"d"日"');
// 2019-07-01 → 令和元年7月1日
// 2020-07-01 → 令和2年7月1日     (変わるのは1年目だけ)

古いファイルが持っている数値形式のタグも認識します。

ws.cell('B3').setFormat('[$-x-gannen411]ggge"年"m"月"d"日"');

タグを付けなければ「令和1年」になります ― これは Excel の挙動と同じで、そして役所の書式では弾かれる表記です。

押さえておきたい点が2つ。ひとつは、このタグは e でも ee でも効くこと。「元」は数ではないのでゼロ埋めのしようがないためです。もうひとつは、タグの効果は それが書かれたセクション に閉じること。複数セクションの書式では、日付を描くセクション側にタグを置きます。


セクション ― 正・負・ゼロ

書式コードは ; で最大4つのセクションに分かれます。先頭3つは値の符号で選ばれます。

正の値 ; 負の値 ; ゼロ
ws.range('B2:E20').setFormat('#,##0;[赤]"▲"#,##0;"−"');
//  1,250,000 → 1,250,000
// -1,250,000 → ▲1,250,000   (赤字)
//          0 → −

ここで効いてくる挙動が 負のセクションには絶対値が渡される ことです。日本の会計表記が成立するのはこのおかげで、"▲"#,##0 と書けば ▲1,250,000 になります(▲-1,250,000 にはなりません)。損益計算書の列にこの書式コードを1つ当てるだけで、日本の財務諸表として読める見た目になり、セルごとの分岐ロジックは要りません。

前年比のようなパーセント列も同じ扱いです。

ws.cell('F5').setFormat('0.0%;[赤]"▲"0.0%');
// 0.443 → 44.3%      -0.088 → ▲8.8%(赤字)

カラーブラケット

角括弧に色名を書くと、そのセクションの文字色が変わります。英名・和名のどちらも使えます。

英名和名
[Black][黒]#000000
[Red][赤]#FF0000
[Green][緑]#008000
[Blue][青]#0000FF
[White][白]#FFFFFF
[Yellow][黄]#FFFF00
[Cyan][水]#00FFFF
[Magenta][紫]#FF00FF

Excel のインデックスカラーも使えます ― [Color 3] または [色3](1〜16)。

解決後の色は取得できます。セルの見た目を UI の別の場所にミラーリングしたいときに便利です。

ws.setCellNumberFormat(5, 2, '#,##0;[赤]"▲"#,##0');
ws.setCellInput(5, 2, '-8000');
ws.getCellFormatColor(5, 2);   // → '#FF0000'

書式の色はセルスタイルの color より優先されます。画面でも、PDF 出力でも同じです(v1.5 の PDF はスタイル側の色を使っていましたが、v1.6 から画面に揃いました)。ただしブラウザ印刷は経路が異なり、HTML 経由で描画されるためセルスタイルの色が使われます ― ブラウザから印刷した帳票では [赤] の赤字が黒くなります。そこが重要な帳票では、スタイルの文字色も併せて設定するか、実スタイルを書き込む条件付き書式で色を付けてください(こちらはどの経路でも反映されます)。


条件付きセクション

符号でセクションを選ばせる代わりに、比較条件を前置きできます。条件は最大2つ+フォールスルー1つです。

ws.range('C2:C50').setFormat('[>=100000]"要審査";[>=10000]"確認";"通常"');

最初にマッチしたセクションが使われ、どれもマッチしなければ 条件が付いていない最初のセクション が使われます。符号ベースの選択と違い、条件付きセクションには値がそのまま渡されます(暗黙の絶対値化はありません)ので、負値ならマイナス記号も出ます。

末尾カンマによる千単位スケーリングと組み合わせると実用的です。カンマ1つにつき表示が 1/1000 になります。

ws.range('D2:D50').setFormat('[>=1000000]#,##0,,"百万円";[>=1000]#,##0,"千円";0"円"');
// 24,500,000 → 25百万円
//  1,250,000 → 1百万円
//      8,500 → 9千円
//        320 → 320円

シートは既定で40行です。最終行を超える範囲は警告もなく無視され、表示形式・スタイル・条件付き書式・入力規則のいずれも黙って落ちます。B2:B100 のような範囲を扱う前に setGridSize() で(あるいはデータ量に合わせて)シートを広げてください。

注意点がひとつ。スケーリング後の数値は書式が要求する桁に丸められるので、#,##0, は 8,500 を「9千円」と表示します ― 正確な金額が必要な列ではなく、サマリー列で使うものです。精度が要るなら小数を足してください(#,##0.0, → 8.5千円)。ただし v1.5 ではスケーリング側を整数パターンにしてください。小数と末尾カンマの組み合わせが効くのは v1.6 以降です。

セルの値そのものは変わりません ― SUM は元の数値で合計します。


通貨とロケールブラケット

通貨記号の付け方は3通りあり、出力は同じです。

ws.cell('B2').setFormat('¥#,##0');                    // ¥1,250,000
ws.cell('B3').setFormat('#,##0"円"');                 // 1,250,000円
ws.cell('B4').setFormat('[$¥-411]#,##0');             // ¥1,250,000

3番目は、通貨にロケールが紐づいているときに Excel が書き出す形式です。[$記号-ロケールID] のうち記号だけが出力され、ロケールIDは落とされます。日本語版 Excel から来たファイルにはこの形式が大量に入っていますが、そのまま読み込めます。

よく使う書式は NumberFormat ヘルパーで生成できます。文字列を毎回手書きする必要はありません。

NumberFormat.number(2);                    // '#,##0.00'
NumberFormat.percent(1);                   // '0.0%'
NumberFormat.currency('¥', 0);             // '¥#,##0'
NumberFormat.currency('円', 0, 'suffix');  // '#,##0円'
NumberFormat.date('yyyy-mm-dd');           // 'yyyy-mm-dd'
NumberFormat.time('h:mm AM/PM');           // 'h:mm AM/PM'

実例 ― 申請書のヘッダー部

ここまでを組み合わせると、官公庁向けの申請書ヘッダーは数行で書けます。

import { createReogrid, NumberFormat } from '@reogrid/lite';

const ws = createReogrid({ workspace: '#grid' }).worksheet;

const WAREKI = '[$-ja-JP-x-gannen]ggge"年"m"月"d"日"';
const YEN    = NumberFormat.currency('¥', 0);
const ACCT   = '#,##0;[赤]"▲"#,##0;"−"';

// 申請日 / 決裁日
ws.cell('B2').setValue('申請日').setStyle({ bold: true });
ws.setCellInput(1, 2, '2019/7/1');
ws.cell(1, 2).setFormat(WAREKI);          // 令和元年7月1日

ws.cell('B3').setValue('決裁日').setStyle({ bold: true });
ws.setCellInput(2, 2, '2024/12/10');
ws.cell(2, 2).setFormat(WAREKI);          // 令和6年12月10日

// 金額欄
ws.cell('B5').setValue('請求金額').setStyle({ bold: true });
ws.setCellInput(4, 2, '1250000');
ws.cell(4, 2).setFormat(YEN);             // ¥1,250,000

ws.cell('B6').setValue('前年差額').setStyle({ bold: true });
ws.setCellInput(5, 2, '-84000');
ws.cell(5, 2).setFormat(ACCT);            // ▲84,000(赤字)

// 規模区分 ― 書式コード1つ、分岐なし
ws.cell('B7').setValue('規模区分').setStyle({ bold: true });
ws.setCellInput(6, 2, '1250000');
ws.cell(6, 2).setFormat('[>=1000000]#,##0,,"百万円";[>=1000]#,##0,"千円";0"円"'); // 1百万円

これらのセルはすべて数値のままです。ソートしても、合計しても、数式から参照しても問題ありません。書式はあくまで表示であり、表示だけを担当します。


どこまで一緒に運ばれるか

画面表示書式のテキストとカラーブラケットの両方が反映される
getDisplayText()書式適用後のテキストを返す
PDF 出力テキストとブラケット色の両方(v1.6 以降)
ブラウザ印刷テキストは反映、色は セルスタイル の色
xlsx インポートカスタム numFmt を読み取って適用(和暦・元年書式を含む)
ReoGrid JSONドキュメントと一緒に往復する

Excel の細かい記法のうちいくつかは、解釈はされますが出力を持ちません。Excel からコピーしてきた書式コードが壊れることはなく、単に余白が付かないだけです ― _c(幅調整プレースホルダー)と *c(塗りつぶし文字)は黙って読み飛ばされます。@(文字列セクション)と [DBNum1] などの漢数字表記には対応していません。


まとめ

日本の帳票には、私たちが使っているどのフレームワークよりも古いルールがあり、そこに交渉の余地はありません ― 1年ではなく元年、マイナス記号ではなく ▲、そして「円」は様式が決めた位置に。Excel は数十年前にそれらを表示形式として符号化しました。ブラウザで同じ要件を満たす最短経路は、同じ言語を話すことです ― セルごとに1つの文字列、値に付随し、グリッドと印刷の両方が尊重する書式コード。

まずは和暦・カラー書式デモでこの記事のコードが実際に描画されるところを確認し、トークンの全一覧は表示形式のドキュメントへ。再現したい様式が今 .xlsx として手元にあるなら、Excel の帳票レイアウトをそのまま Web へから始めてください(書式も一緒に移ってきます)。最終的に紙にする必要があるなら、ブラウザだけで請求書PDFを生成するが続きです。

ReoGrid Web を試してみる

React/Vue 向けの Canvas ベース Excel 互換スプレッドシートコンポーネント。 Lite は無料 — npm install 一発で始められます。

関連記事

JavaScriptスプレッドシートの VLOOKUP・XLOOKUP・SUMIFS ― 単価も集計も自動で追随する受注明細表

商品コードから単価を引き、区分ごとに集計する受注明細表をブラウザで組み立てる。VLOOKUP と XLOOKUP の違い、マスタに無いコードの扱い(if_not_found・IFNA・IFERROR)、SUMIF / SUMIFS / COUNTIFS の criteria 記法と引数順の落とし穴、そして ReoGrid Web に「無い」2つの参照関数まで。

ブラウザだけで請求書PDFを生成する ― テンプレート1枚に、データを流し込む

PDF帳票のパイプラインを丸ごとブラウザの中で完結させる方法。{{token}} セルで帳票を一度設計し、プレーンなデータオブジェクトをバインドして明細行を件数ぶん展開、saveAsPdf でベクターPDFとして出力。改ページ、ページヘッダー・フッター、行ごとの数式、そして1枚のテンプレートから100通を作るバッチ処理まで。

JavaScriptスプレッドシートでピボットテーブル ― 数行のAPIで、ライブ更新されるクロス集計を

数百行の生データを、データの変更に追随して再計算されるクロス集計表に。createPivot 一発で行・列・集計を宣言し、ハンドルの update / refresh / remove でコードから操作。地域×商品の売上を使った完全な実例と、フィルター・総計・Lite 対 Pro の境界線まで。

ニュースレター

開発の最新情報をお届けします

新しいリリース・機能追加・お知らせをいち早く受け取るには、
メーリングリストにご登録ください。