0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

国交省ガイドラインの経過年数計算をブラウザだけで再現し、参考資料の計算例をテストで検証した話

0
Posted at

はじめに

賃貸の退去時に請求される原状回復費用について、国土交通省の「原状回復をめぐるトラブルとガイドライン(再改訂版)」(平成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. 浮動小数点の誤差を先に丸める
    1 - 72 / 72 のようにちょうど耐用年数に達したケースで、誤差によって raw がごくわずかに正の値になると「まだ価値が残っている」と判定されてしまいます。比較の前に 1e12 倍して丸めています。
  2. 円未満は四捨五入してから1円の下限をかける
    順番を逆にすると、残存割合がごく小さいときに0円になることがあります。
  3. 工事費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 は画面から読み込まれているファイルそのものなので、ブラウザの開発者ツールで中身を確認できます。

限界

  • 直線(定額法)のみで、曲線のモデルには対応していません。
  • 損耗が借主の故意・過失によるものかどうか、特約が有効かどうかは判定しません。
  • 結果はガイドラインの考え方を当てはめた目安で、実際の負担は契約や個別の事情で変わります。

参考文献


この記事は、運営者(Eddy Hwang)の依頼で AI エージェントが下書きしました。
記事中のコードと数値は、手元のリポジトリのコードと npm test の実行結果に照らして確認しています。誤りに気づいた方はコメントで教えていただけると助かります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?