今回は paiza のクエリメニューで「経理」の問題に挑戦!
オブジェクトは順番通りに列挙されるかどうかわからないことに注意!
あと、for…in の存在は、学び初めの方に for…of と一緒に知っていたけど、実際に使ってみて、オブジェクトに使うものだとわかった!(結局、無意味だったけど(´;ω;`))
問題概要
-
paiza には N 個の部署 があり、各部署には固有の名前 S₁, S₂, …, Sₙ が付いている。
-
あなたは 経理係 として、部署ごとに行った買い物の内容を記録し、まとめる役目を担っている。
-
会社には K 枚の領収書 があり、各領収書には次の情報が含まれている:
- 部署名(どの部署の買い物か)
- 領収書番号(取引を一意に識別する番号)
- 金額(円)
-
あなたの仕事は、各部署ごとに、その部署が発行した領収書を入力順に整理し、部署名とともに会計表として出力すること。
入力例:
4 5
ed
bjd
bdkf
fkoe
ed 20 2093
ed 584 3388
ed 31737 3885
ed 023748 9300
fkoe 82928 274
出力例:
ed
20 2093
584 3388
31737 3885
023748 9300
-----
bjd
-----
bdkf
-----
fkoe
82928 274
-----
❌ NG例:
const rl = require('readline').createInterface({input:process.stdin});
const lines = [];
rl.on('line', (input) => lines.push(input));
rl.on('close', () => {
const [N, K] = lines[0].split(' ').map(Number);
const sections = {};
for(let i = 1; i <= N; i++){
const key = lines[i];
sections[key] = {};
}
for(let i = N + 1; i <= N + K; i++){
const [name, num, price] = lines[i].split(' ');
const key = name;
sections[key][num] = Number(price);
}
for(let i = 1; i <= N; i++){
const key = lines[i];
const section = sections[key];
console.log(key);
for(const num in section){
console.log(num, section[num]);
}
console.log('-----')
}
});
正直、NG例のどこがNGなのかはっきりとはわからなかった。
なぜなら、エラーは出ず、テストケースも入力例1と2は通過(50点)しているので、出力形式のミスでもなさそうだからである。
🔍なぜNGになるか(推理)
🔍オブジェクトのキーの列挙順が仕様依存であるから。
JavaScriptのオブジェクトのキー列挙は以下のルールがあるため、
- 数値として解釈できるキーは昇順で列挙される
- それ以外は追加順に列挙される
注文番号 num は文字列(例: “023748” や “1”)でも、数値っぽい文字列は昇順に並べ替えられてしまう可能性がある。
つまり、入力の順番通りではなく、キーの「数値っぽさ」によって並び順が勝手に変わってしまう。
🔍 重要なのは、「順番は保証されない」ということ。
ここが問題で不正解となった可能性が高い(´;ω;`)
✅ OK例:
const sections = {};
// 部署名ごとに、領収書リストを配列で持つ
for (let i = 1; i <= N; i++) {
const key = lines[i];
sections[key] = []; // 空配列にする
}
for (let i = N + 1; i <= N + K; i++) {
const [name, num, price] = lines[i].split(' ');
sections[name].push([num, Number(price)]); // 配列に順番に追加
}
for (let i = 1; i <= N; i++) {
const key = lines[i];
const section = sections[key];
console.log(key);
section.forEach(([num, price]) => {
console.log(num, price);
});
console.log('-----');
}
-
023748のような注文番号もあるので、注文番号は数値型に変換しないように注意する。
- 会社名でオブジェクトのプロパティ名(キー)を登録して、管理(
key=nameで動的にアクセス)
-
section.forEach(([num, price])は分割代入。
配列の要素は[num, price]の形になっているので、そのままnumとpriceに分割して代入して、簡単に取り出せるようにしている。
- 二次元配列にして管理
例えば、部署 A に2件領収書があるとき:
sections["A"].push(["1", 100]);
sections["A"].push(["2", 200]);
これで
sections["A"] === [ ["1", 100], ["2", 200] ]
となり、領収書が入力順に並んだ配列として保持される。
✅ OK例:Map()バージョン
const rl = require('readline').createInterface({ input: process.stdin });
const lines = [];
rl.on('line', (input) => lines.push(input));
rl.on('close', () => {
const [N, K] = lines[0].split(' ').map(Number);
const sections = new Map();
// 部署名を Map にセット(空配列で初期化)
for (let i = 1; i <= N; i++) {
const sectionName = lines[i];
sections.set(sectionName, []);
}
// 領収書を該当部署の配列に push
for (let i = N + 1; i <= N + K; i++) {
const [name, num, price] = lines[i].split(' ');
sections.get(name).push([num, Number(price)]);
}
// 出力
for (const [sectionName, receipts] of sections) {
console.log(sectionName);
receipts.forEach(([num, price]) => {
console.log(num, price);
});
console.log('-----');
}
});
🗒️まとめ
- ⚠️ オブジェクトは順番通り列挙されることが保証されない。
- 配列はインデックスの順番、
push()での挿入順に列挙されることが保証される。
💡おまけ
今回問題を解くのに使ったものについて、復習とメモ。
✏️ ブラケット記法の復習
1️⃣ ブラケット記法でプロパティを追加する
ブラケット記法は プロパティ名を動的に扱いたいとき に便利!
const key = "name";
const obj = {};
obj[key] = "Alice"; // ここがブラケット記法!
console.log(obj); // { name: "Alice" }
2️⃣ ドット記法との違い
- ドット記法 →
obj.name = “Alice”→ キーが固定のときに使う - ブラケット記法 →
obj[key] = “Alice”→ キーを変数で動的に決めたいときに使う
3️⃣ テンプレートリテラルもOK
const num = 1;
const obj = {};
obj[`group_${num}`] = "Group One";
console.log(obj); // { group_1: "Group One" }
✏️ for…in
| for...in | for...of | |
|---|---|---|
| 対象 | オブジェクトのキー(プロパティ名) | Iterable(繰り返せるもの) → 配列・文字列・Map・Set |
| 何を返す? | キー(プロパティ名) | 値(要素) |
| 用途 | オブジェクトを列挙する | 配列や文字列を要素順に処理 |
✅ 使い方の比較
🔹 for…in は「オブジェクトのキー回し」
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
console.log(key); // 'a', 'b', 'c'
console.log(obj[key]); // 1, 2, 3
}
- オブジェクトのプロパティ名が順番に取り出される。
- 配列にも使えるけど非推奨!(理由は後で 👇)
🔹 for…of は「配列などの要素回し」
const arr = [10, 20, 30];
for (const value of arr) {
console.log(value); // 10, 20, 30
}
- 値(要素)そのものが順に取り出される。
- 配列、文字列、
Map、Setなど Iterable にだけ使える。
- オブジェクト単体には使えない!(エラーになる)
const obj = { a: 1, b: 2 };
for (const v of obj) {
// ❌ TypeError: obj is not iterable
}
✅ 配列に for…in は非推奨
const arr = ["a", "b", "c"];
for (const i in arr) {
console.log(i); // '0', '1', '2' ← インデックスが文字列で出てくる
console.log(arr[i]); // 'a', 'b', 'c'
}
これも一応動くけど、
- インデックス(添字)が文字列として返る
-
for…inはオブジェクトのプロパティ全体を見るので、配列に拡張プロパティを追加してるとそれも拾っちゃう - だから配列には
for…ofが安全
✅ まとめ:どっちを使う?
| 使いたいもの | 推奨 |
|---|---|
| 配列の要素 | ✅ for...of |
| 文字列の文字 | ✅ for...of |
| Map / Set | ✅ for...of |
| オブジェクトのキー | ✅ for...in |
| オブジェクトの値 | Object.entries + for...of |
✅ 実例:オブジェクトを for…of するなら Object.entries
const obj = { a: 1, b: 2 };
for (const [key, value] of Object.entries(obj)) {
console.log(key, value);
}
// a 1
// b 2
🔑 ポイント
- 配列 →
for…of - オブジェクト →
for…inまたはObject.entries+for…of