時刻を 09:05、番号を No.0007 のように、桁をそろえて表示したい。そんな「ゼロ埋め」には、よく次の2つの書き方が使われます。
const n = 5;
("0" + n).slice(-2); // "05"
String(n).padStart(2, "0"); // "05"
1桁の数字なら、どちらも同じ結果です。ところが、桁があふれたとき、マイナスや小数のとき、日付の月を埋めるときには、エラーにならないまま結果がずれる場面がありました。この記事では、Node.js v24.14.1 で実際に動かした結果をもとに整理します。結果は、コードのまとまりごとに .mjs ファイルに保存して node で実行し、console.log で出した値をコメントに書いています。
桁があふれると、slice(-2) は上の桁を黙って捨てる
padStart(2, "0") は「2文字に足りなければ、左に "0" を足す」という意味です。もとから2文字以上あれば、何もしません。一方、slice(-2) は「後ろから2文字だけ取り出す」なので、3桁になると先頭が消えます。
String(123).padStart(2, "0"); // "123"
("0" + 123).slice(-2); // "23"
秒数を「分:秒」にする処理で比べました。100分を超えると、違いがはっきり出ます。
const sec = 6005; // 100分5秒
`${String(Math.floor(sec / 60)).padStart(2, "0")}:${String(sec % 60).padStart(2, "0")}`;
// "100:05"
`${("0" + Math.floor(sec / 60)).slice(-2)}:${("0" + (sec % 60)).slice(-2)}`;
// "00:05"
slice(-2) 版は、100分5秒を 00:05 と表示しました。エラーは出ないので、短い動画や練習用のデータで試しているうちは気づきにくいところです。桁があふれる可能性があるなら、padStart のほうが安全です。
数値のまま padStart を呼ぶと TypeError
padStart は文字列のメソッドです。数値のまま呼ぶとエラーになりました。
const n = 5;
n.padStart(2, "0");
// TypeError: n.padStart is not a function(Node.js での表示)
String(n).padStart(2, "0"); // "05"
`${n}`.padStart(2, "0"); // "05"
先に String() かテンプレートリテラルで文字列にしてから呼びます。
マイナスと小数は、思った形にならない
padStart は、文字列の中身を数字として見ていません。左に "0" を足すだけなので、マイナスの記号や小数点も1文字として数えます。
String(-5).padStart(3, "0"); // "0-5"
String(5.5).padStart(2, "0"); // "5.5"
String(5.25).padStart(4, "0"); // "5.25"
-5 は "-05" ではなく、記号の前に 0 が入った "0-5" になりました。
小数は、整数の部分だけを2桁にしたくても、小数点と小数の桁まで数えます。5.5 は幅2では何も足されず、5.25 は幅4でも整数の部分が埋まりませんでした。必要な幅が、小数の桁数によって変わります。
気温や前日比のように、マイナスになりうる数を埋めるときは、符号と数字を分けて埋めます。
const n = -5;
(n < 0 ? "-" : "") + String(Math.abs(n)).padStart(2, "0"); // "-05"
Intl.NumberFormat の minimumIntegerDigits(整数部の最低桁数)を使う方法もあります。
const nf = new Intl.NumberFormat("ja-JP", { minimumIntegerDigits: 2 });
nf.format(5); // "05"
nf.format(-5); // "-05"
nf.format(5.25); // "05.25"
nf.format(1234); // "1,234"
マイナスはそのまま "-05" になり、小数も整数の部分だけが2桁になりました。一方で、1234 には3桁ごとのカンマが入りました。番号のように、カンマを入れたくない場面では useGrouping: false を足します。
new Intl.NumberFormat("ja-JP", { minimumIntegerDigits: 2, useGrouping: false }).format(1234);
// "1234"
もう1つ、何も指定しないと小数は3桁までに丸められました。小数の桁をそのまま残したいときは、maximumFractionDigits で桁数を指定します。
nf.format(1.23456); // "01.235"
new Intl.NumberFormat("ja-JP", { minimumIntegerDigits: 2, maximumFractionDigits: 5 }).format(1.23456);
// "01.23456"
値が undefined や null でも、エラーにならない
String(undefined).padStart(2, "0"); // "undefined"
String(null).padStart(2, "0"); // "null"
どちらも2文字以上あるので、そのまま返りました。データがまだ届いていないときに、画面に undefined と出る原因になります。埋める前に、値があるかを確かめます。
const n = null;
n == null ? "--" : String(n).padStart(2, "0"); // "--"
n == null は、null と undefined の両方で true になる書き方です。理由は 「==は使うな」の唯一の例外。x == null だけは使っていい理由 で書きました。
日付の月は、+1 を忘れると「00月」になる
Date の getMonth() は、1月を 0 として返します。
const d = new Date(2026, 0, 5, 9, 3, 7); // 2026年1月5日 9:03:07
d.getMonth(); // 0
String(d.getMonth()).padStart(2, "0"); // "00"(+1 を忘れた)
String(d.getMonth() + 1).padStart(2, "0"); // "01"
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
// "2026-01-05"
Date の月まわりで、1月31日の1か月後が3月3日になる話は 1月31日の「1か月後」が3月3日になる — Date の月の繰り上がりと月末の扱い で書きました。
日付と時刻なら、toLocaleDateString / toLocaleTimeString に "2-digit" を指定して、埋めてもらう方法もあります。
d.toLocaleDateString("ja-JP", { year: "numeric", month: "2-digit", day: "2-digit" });
// "2026/01/05"
d.toLocaleTimeString("ja-JP", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
// "09:03:07"
d.toLocaleTimeString("ja-JP", { hour: "2-digit" });
// "09時"
時だけを指定すると、"09時" のように「時」が付きました(どれも Node.js v24.14.1 で確かめた出力です)。区切りの記号や文字は地域の設定(ここでは "ja-JP")で決まるので、形を決め打ちで使うなら、使う環境で出力を一度確かめます。たくさんの日付をまとめて整形するときの速さは、toLocaleString を2万回呼ぶと遅い? Node.js と WebKit で Intl.DateTimeFormat 使い回しと実測 で比べました。
ゼロ埋めすると、文字列の並べ替えがそろう
ファイル名や番号を文字列のまま並べ替えると、10 が 2 より前に来ます。文字列は、1文字目から順に比べるためです。
["img10.png", "img2.png", "img1.png"].sort();
// [ 'img1.png', 'img10.png', 'img2.png' ]
["img10.png", "img02.png", "img01.png"].sort();
// [ 'img01.png', 'img02.png', 'img10.png' ]
"9" < "10"; // false
"09" < "10"; // true
桁をそろえると、文字列のままでも数の順に並びました。ただし、埋めた桁数より桁の多い番号(2桁で埋めたところに100番など)が混ざると、また崩れます。
["img10.png", "img02.png", "img100.png", "img11.png", "img01.png"].sort();
// [ 'img01.png', 'img02.png', 'img10.png', 'img100.png', 'img11.png' ]
2桁で埋めているところに img100.png が入ると、img10.png と img11.png の間に並びました。最初の章の「桁があふれる」と同じで、いちばん大きい番号の桁数で埋める必要があります。名前を変えられないときは、localeCompare の numeric: true で、数字の部分を数として比べられます。
["img10.png", "img2.png", "img1.png"].sort((a, b) =>
a.localeCompare(b, undefined, { numeric: true })
);
// [ 'img1.png', 'img2.png', 'img10.png' ]
padStart の細かいところ
"5".padStart(3); // " 5"(第2引数を省くと半角スペース)
"5".padStart(3, ""); // "5"(空の文字列だと何も足さない)
"5".padStart(6, "ab"); // "ababa5"(足りない分だけ繰り返して切る)
"abc".padEnd(6, "."); // "abc..."(右に足すのは padEnd)
全角の数字や絵文字も、文字数の数え方は length と同じです。
"5".padStart(2, "0"); // "05"(全角の5は1文字)
"😀".padStart(3, "0"); // "0😀"(😀は length が2)
😀 は見た目は1文字ですが、length は 2 なので、"0" は1つしか足されませんでした。絵文字の length がずれる理由は、絵文字を含む文字列の length が思った数にならない理由と、正しい文字数の数え方 で書きました。
AI が書いたゼロ埋めを見るときの確認ポイント
-
("0" + n).slice(-2)の形になっていて、3桁以上になる値(分・番号・金額など)を渡していないか - マイナスになりうる値を、そのまま
padStartしていないか - 小数を埋めるときに、幅を小数の桁まで含めて考えているか(小数の桁数がばらつくなら
Intl.NumberFormatを使う) - 並べ替えのためにゼロ埋めするなら、いちばん大きい番号の桁数で埋めているか
-
getMonth()に+ 1があるか - 埋める前に、値が
undefinedやnullでないかを確かめているか
未経験者向けの講座を運営しています
未経験から Next.js + Supabase + Claude Code で Webアプリを公開するまで を、全24セッションで体系化した教材です。Claude Code を学習パートナーにする CLAUDE.md と学習モード(learner / developer)の設計までセットで含みます。
- ▶ ライブデモ(インストール不要・無料で今すぐ見られます)→ https://trial-web-ayies128s-projects.vercel.app/?ref=qiita
- 教材完全版+月5,500円のメンタリング(全24セッション+チャット質問し放題)→ https://menta.work/plan/20251?ref=qiita
- 無料体験版(git clone して自分の手元で動かす・最初の数セッション分・⭐ Star もよろしくお願いします)→ https://github.com/ayies128/next-ai-camp-trial
- YouTube『AIエンジニア情報局』(AI×開発ニュースを1本5分でキャッチアップできる別運営チャンネル・無料)→ https://www.youtube.com/channel/UC1rXVD9WYsQPQEWZyd-A1KA/?ref=qiita
※ 中上級者には易しすぎる内容なので、初心者の知り合いへの紹介や社内研修の参考としてどうぞ。