はじめに
賃貸の退去時に請求される原状回復費用について、国土交通省の「原状回復をめぐるトラブルとガイドライン(再改訂版)」(平成23年8月)は、壁紙などの価値が年数とともに下がる「経過年数」の考え方を示しています。
この計算を、ブラウザだけで動く小さな ES モジュールにしました。
あわせて、同ガイドラインの「参考資料」(令和5年3月)に載っている計算例を node:test でそのまま再現し、数字が一致するかを確かめています。
この記事では、計算ロジックの組み立て方と、公的資料の計算例をテストにするときに気をつけた点を書きます。
私(Eddy Hwang)が作って運営している計算ツールの中身の話です。
法律上の判断をするものではなく、ガイドラインの計算方法を当てはめた目安を出すだけのものです。
ガイドラインの考え方(コードにした部分)
ガイドライン本文で、計算に関係するのは主に次の箇所です。
| 箇所 | 内容 |
|---|---|
| p.12 | 耐用年数が経過した時点で残存価値が1円になるような直線(または曲線)で、借主の負担割合を決める |
| p.13 | 入居時点で新品でない場合は、その状態に合わせてグラフを下方(左方)にシフトさせる。1円が下限 |
| p.14 | フローリングの部分補修、襖紙・障子紙・畳表などは経過年数を考慮しない |
| p.22〜24 | 別表2:部位ごとの耐用年数 |
| p.168 | 資料8:建物の構造ごとの耐用年数 |
コードでは直線(定額法)だけを実装しています。曲線のモデルは扱っていません。
設計:ブラウザとテストで同じファイルを使う
計算は engine.mjs という依存ゼロの ES モジュール1つにまとめました。
画面側の app.mjs はこれを import するだけで、入力値はどこにも送信せず、保存もしません。
Node.js のテストも同じファイルを import するので、テストで確かめた関数がそのままブラウザで動きます。
耐用年数の表も engine.mjs の配列が唯一の定義です。
// life: 年数、'building' = 建物の耐用年数を使う、null = 経過年数を考慮しない
export const ITEMS = [
{ id: 'cloth', life: 6, page: 'p.23', ja: '壁・天井のクロス(壁紙)' },
{ id: 'tatamiomote', life: null, page: 'p.14, p.22', ja: '畳表(表替え・裏返し)' },
{ id: 'flooringpart', life: null, page: 'p.14, p.22', ja: 'フローリング(部分補修)' },
{ id: 'flooringall', life: 'building', page: 'p.14, p.22', ja: 'フローリング(全体の張替え)' },
{ id: 'sink', life: 5, page: 'p.24', ja: '流し台' },
// ...(実際は20項目)
];
export const BUILDINGS = [
{ id: 'wood', life: 22, ja: '木造・合成樹脂造(住宅用)' },
{ id: 'rc', life: 47, ja: '鉄筋コンクリート造・鉄骨鉄筋コンクリート造(住宅用)' },
// ...(実際は7種類)
];
(記事用に英語名などのプロパティを省いています)
「年数」「建物に従う」「考慮しない」の3種類を life の型で分けておくと、計算関数の分岐が単純になります。
月数は「年月の差」で数える
入居期間は日付ではなく「年月」で受け取り、月の差を取ります。
/** 'YYYY-MM' -> 0年からの通算月、不正なら NaN */
export function ymIndex(ym) {
const m = /^(\d{4})-(\d{2})$/.exec(String(ym || '').trim());
if (!m) return NaN;
const mo = Number(m[2]);
if (mo < 1 || mo > 12) return NaN;
return Number(m[1]) * 12 + (mo - 1);
}
/** 入居月から退去月までの月数(例:2021-04 → 2025-12 = 56) */
export function monthsBetween(fromYm, toYm) {
const a = ymIndex(fromYm), b = ymIndex(toYm);
if (Number.isNaN(a) || Number.isNaN(b)) return NaN;
return b - a;
}
参考資料 p.17 の「4年8か月」は56か月です。日単位の端数をどう扱うかで悩まなくて済むよう、入力をはじめから年月にしました。
1行分の負担割合を出す関数
中心になるのは次の関数です(引数チェックの一部を省略しています)。
export function lineShare({ cost, monthsLived, life, start = 'new', ageMonths = 0, valuePct = 100 }) {
if (life === null) {
// 経過年数を考慮しない項目は全額
return { depreciated: false, ratio: 1, amount: Math.round(cost), landlord: 0 };
}
const lifeMonths = life * 12;
let startRatio = 1;
if (start === 'age') {
// 入居時にすでに経過していた月数のぶん、グラフを左にずらす
startRatio = Math.max(0, 1 - ageMonths / lifeMonths);
} else if (start === 'value') {
// 入居時の価値を合意している場合(例:80%)はグラフを下にずらす
startRatio = valuePct / 100;
}
const raw = Math.round((startRatio - monthsLived / lifeMonths) * 1e12) / 1e12;
const expired = raw <= 0;
const ratio = expired ? 0 : raw;
// 残存価値は1円が下限。ただし工事費0円なら0円のまま
const amount = cost === 0 ? 0 : Math.max(1, Math.round(cost * ratio));
return { depreciated: true, ratio, amount, landlord: Math.max(0, Math.round(cost) - amount), expired };
}
ポイントは3つです。
-
浮動小数点の誤差を先に丸める
1 - 72 / 72のようにちょうど耐用年数に達したケースで、誤差によってrawがごくわずかに正の値になると「まだ価値が残っている」と判定されてしまいます。比較の前に1e12倍して丸めています。 -
円未満は四捨五入してから1円の下限をかける
順番を逆にすると、残存割合がごく小さいときに0円になることがあります。 -
工事費0円は0円のまま
1円の下限を機械的にかけると、0円の行が1円になってしまうので分けています。
参考資料の計算例をテストにする
ここがこの記事でいちばん伝えたいところです。
公的資料に計算例があるなら、それをテストケースにしておくと、実装の誤りに早く気づけます。
テスト名に資料のページ番号を入れておくと、あとで根拠をたどるのも楽です。
import test from 'node:test';
import assert from 'node:assert/strict';
import { lineShare, lifeFor, itemById } from '../static/engine.mjs';
const near = (a, b, eps = 0.0005) => assert.ok(Math.abs(a - b) < eps, `${a} != ${b}`);
test('wallpaper example: 20㎡ × 1,200円, 4 years from new = 8,000円 (参考資料 p.18)', () => {
assert.equal(lineShare({ cost: 20 * 1200, monthsLived: 48, life: 6 }).amount, 8000);
});
test('whole flooring, wooden building aged 10 at move-in + 4 years: 195,000円 → 70,909円 (参考資料 p.21)', () => {
const life = lifeFor(itemById('flooringall'), { buildingId: 'wood' });
assert.equal(life, 22);
const r = lineShare({ cost: 13 * 15000, monthsLived: 48, life, start: 'age', ageMonths: 120 });
near(r.ratio, 1 - 14 / 22);
assert.equal(r.amount, 70909);
assert.equal(r.landlord, 124091);
});
再現している例をまとめると次のとおりです。
| 資料 | 条件 | テストで確かめていること |
|---|---|---|
| 参考資料 p.17 | 壁紙(耐用年数6年)を新品から3年 | 残存割合 50% |
| 参考資料 p.17 | 新品から4年8か月(56か月) | 残存割合 1 − 56/72 |
| 参考資料 p.17 | 入居時に3年経過、そこから2年 | 経過は合計60か月、割合 1 − 60/72 |
| 参考資料 p.17、ガイドライン p.13 | 入居時の価値を80%と合意 | 0.8 − 経過月数/72 |
| 参考資料 p.18 | 壁紙 20㎡ × 1,200円、新品から4年 | 借主負担 8,000円 |
| 参考資料 p.18 | 壁紙 40㎡ × 1,200円 = 48,000円、4年 | 借主 16,000円、貸主 32,000円 |
| 参考資料 p.21 | フローリング部分補修 2㎡ × 15,000円 | 経過年数を考慮せず 30,000円 |
| 参考資料 p.21 | 全体張替え、木造で築10年時に入居して4年 | 195,000円のうち 70,909円 |
| 参考資料 p.23 | 全体張替え、RC造で新築から20年 | 27/47(57.4%)、86,170円 |
このほか、耐用年数を過ぎた場合に1円になること、別表2と資料8の耐用年数が配列と一致すること、複数行の合計と敷金との差額、入力エラーの扱いもテストにしています。
計算エンジンのテストは12件、ページ生成側のテスト(出典ページ番号の記載、リンク先のページ番号など)が15件で、npm test で27件すべて通ることを確認しています(2026年10月31日時点)。
ℹ tests 27
ℹ pass 27
ℹ fail 0
テストを書いてみて気づいたこと
-
PDF の印刷ページ番号と PDF のページ番号がずれる
ガイドライン本文の PDF は「印刷ページ = PDF ページ − 4」、参考資料は「− 5」でした。根拠のリンクを張るときはこの差を足しています。ページ番号のずれもテストで検査しています。 -
資料の例は「割合」で示されていることがある
割合だけの例はnear()で比率を比べ、金額まで示されている例はassert.equalで円単位まで一致させています。 -
木造の例は建物の耐用年数を使う
フローリング全体の張替えは、部位の年数ではなく建物の構造の耐用年数(木造22年、RC造47年)を使います。ここを部位の表に直接書かず'building'という印にしたので、構造の選び忘れを例外で検出できます。
実際に動くもの
この engine.mjs を使った計算画面を公開しています。入力した金額や年月はブラウザの外に送りません。
engine.mjs は画面から読み込まれているファイルそのものなので、ブラウザの開発者ツールで中身を確認できます。
限界
- 直線(定額法)のみで、曲線のモデルには対応していません。
- 損耗が借主の故意・過失によるものかどうか、特約が有効かどうかは判定しません。
- 結果はガイドラインの考え方を当てはめた目安で、実際の負担は契約や個別の事情で変わります。
参考文献
- 国土交通省「原状回復をめぐるトラブルとガイドライン(再改訂版)」平成23年8月
https://www.mlit.go.jp/common/001016469.pdf - 国土交通省「原状回復をめぐるトラブルとガイドラインに関する参考資料」令和5年3月
https://www.mlit.go.jp/jutakukentiku/house/content/001611293.pdf
この記事は、運営者(Eddy Hwang)の依頼で AI エージェントが下書きしました。
記事中のコードと数値は、手元のリポジトリのコードと npm test の実行結果に照らして確認しています。誤りに気づいた方はコメントで教えていただけると助かります。