找回密码
 立即注册
首页 业界区 业界 用 C# 写个人住房贷款计算器

用 C# 写个人住房贷款计算器

啪炽 2025-5-30 01:05:40
现在,很多人都有个人住房贷款,或者将要有个人住房贷款。那么,就让我们用 C# 写一个计算器,用于计算个人住房贷款的还款计划表。
1.png

这个计算器能够根据你给出的贷款金额、贷款期数、贷款日期、还款方式、贷款种类,计算出相应的还款计划表,如上图所示。
这样,就很容易知道每月要还多少钱,到现在为止剩余多少贷款未还,最终要付出多少贷款利息,等等。
 
贷款利率是由贷款种类决定的,存放在 LoanCalculator.xml 文件中:
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <LoanCalculator>
  3.   <option balance="13.8" months="180" date="2004-07-23" method="等本息" item="公积金" />
  4.   <items>
  5.     <item title="公积金">
  6.       <rate date="1001-01-01" low="3.6"  high="4.05" />
  7.       <rate date="2005-01-01" low="3.78" high="4.23" />
  8.       <rate date="2006-01-01" low="3.96" high="4.41" />
  9.       <rate date="2007-01-01" low="4.14" high="4.59" />
  10.       <rate date="2008-01-01" low="5.04" high="5.22" />
  11.       <rate date="2009-01-01" low="3.33" high="3.87" />
  12.     </item>
  13.     <item title="商业性基准">
  14.       <rate date="1001-01-01" low="5.51"  high="5.75" />
  15.       <rate date="2007-01-01" low="5.51"  high="5.81" />
  16.       <rate date="2008-01-01" low="6.579" high="6.65" />
  17.       <rate date="2009-01-01" low="5.76"  high="5.94" />
  18.     </item>
  19.     <item title="商业性优惠">
  20.       <rate date="1001-01-01" low="5.51"  high="5.75" />
  21.       <rate date="2007-01-01" low="5.51"  high="5.81" />
  22.       <rate date="2008-01-01" low="6.579" high="6.65" />
  23.       <rate date="2009-01-01" low="4.03"  high="4.16" />
  24.     </item>
  25.   </items>
  26. </LoanCalculator>
复制代码
你可以自行修改这个文件,以适应不同银行的贷款利率。
 
这个文件由 Config.cs 文件中的 Config 类读取:
  1. using System;
  2. using System.Xml;
  3. using System.Drawing;
  4. using System.Collections.Generic;
  5. namespace Skyiv.Ben.LoanCalculator
  6. {
  7.   sealed class Config
  8.   {
  9.     static readonly string ElmOption = "option";
  10.     static readonly string ElmItems = "items";
  11.     static readonly string AttrBalance = "balance";
  12.     static readonly string AttrMonths = "months";
  13.     static readonly string AttrDate = "date";
  14.     static readonly string AttrMethod = "method";
  15.     static readonly string AttrItem = "item";
  16.     static readonly string AttrTitle = "title";
  17.     static readonly string AttrLow = "low";
  18.     static readonly string AttrHigh = "high";
  19.     public decimal Balance { get; private set; } // 贷款金额(万元)
  20.     public int Months { get; private set; }      // 贷款期数(月)
  21.     public DateTime Date { get; private set; }   // 贷款日期
  22.     public bool IsEq { get; private set; }       // 还款方式: true:等本息  false: 等本金
  23.     public string Item { get; private set; }     // 贷款种类
  24.     public string[] Items { get; private set; }  // 贷款种类列表
  25.     public KeyValuePair<DateTime, PointF>[] Rates { get; private set; } // 贷款利率
  26.     KeyValuePair<DateTime, PointF>[][] ratesArray; // 各种类的“贷款利率”列表
  27.     public Config(string fileName)
  28.     {
  29.       try
  30.       {
  31.         var doc = new XmlDocument();
  32.         doc.Load(fileName);
  33.         var elm = doc.DocumentElement[ElmOption];
  34.         if (elm == null) throw new Exception("未能找到 <" + ElmOption + "> 元素");
  35.         Balance = GetDecimal(elm, AttrBalance);
  36.         Months = GetInt32(elm, AttrMonths);
  37.         Date = GetDateTime(elm, AttrDate);
  38.         IsEq = GetBooleanFromMethod(elm, AttrMethod);
  39.         Item = GetString(elm, AttrItem);
  40.         Items = GetItemsAndLoadRatesArray(doc);
  41.         SetRates(Item);
  42.       }
  43.       catch (Exception ex)
  44.       {
  45.         throw new Exception("读配置文件(" + fileName + ")", ex);
  46.       }
  47.     }
  48.     // 根据贷款种类设置贷款利率
  49.     public void SetRates(string key)
  50.     {
  51.       var idx = Array.IndexOf(Items, key);
  52.       if (idx < 0) throw new Exception("无此贷款种类: " + key);
  53.       Rates = ratesArray[idx];
  54.     }
  55.     string[] GetItemsAndLoadRatesArray(XmlDocument doc)
  56.     {
  57.       var elm = doc.DocumentElement[ElmItems];
  58.       if (elm == null) throw new Exception("未能找到 <" + ElmItems + "> 元素");
  59.       var elms = elm.ChildNodes;
  60.       var items = new string[elms.Count];
  61.       ratesArray = new KeyValuePair<DateTime, PointF>[elms.Count][];
  62.       for (var i = 0; i < elms.Count; i++)
  63.       {
  64.         items[i] = GetString(elms[i], AttrTitle);
  65.         ratesArray[i] = GetRates(elms[i]);
  66.       }
  67.       return items;
  68.     }
  69.     KeyValuePair<DateTime, PointF>[] GetRates(XmlNode elm)
  70.     {
  71.       var elms = elm.ChildNodes;
  72.       var rates = new KeyValuePair<DateTime, PointF>[elms.Count];
  73.       for (var i = 0; i < elms.Count; i++)
  74.         rates[i] = new KeyValuePair<DateTime, PointF>(GetDateTime(elms[i], AttrDate),
  75.           new PointF(GetFloat(elms[i], AttrLow), GetFloat(elms[i], AttrHigh)));
  76.       return rates;
  77.     }
  78.     string GetString(XmlNode elm, string key)
  79.     {
  80.       if (elm.Attributes[key] == null) throw new Exception("未能找到 <" + elm.Name + "> 元素的 " + key + " 属性");
  81.       return elm.Attributes[key].Value;
  82.     }
  83.     float GetFloat(XmlNode elm, string key)
  84.     {
  85.       float value;
  86.       if (!float.TryParse(GetString(elm, key), out value))
  87.         throw new Exception("<" + elm.Name + "> 元素的 " + key + " 属性的值必须为浮点数");
  88.       return value;
  89.     }
  90.     decimal GetDecimal(XmlNode elm, string key)
  91.     {
  92.       decimal value;
  93.       if (!decimal.TryParse(GetString(elm, key), out value))
  94.         throw new Exception("<" + elm.Name + "> 元素的 " + key + " 属性的值必须为实数");
  95.       return value;
  96.     }
  97.     int GetInt32(XmlNode elm, string key)
  98.     {
  99.       int value;
  100.       if (!int.TryParse(GetString(elm, key), out value))
  101.         throw new Exception("<" + elm.Name + "> 元素的 " + key + " 属性的值必须为整数");
  102.       return value;
  103.     }
  104.     DateTime GetDateTime(XmlNode elm, string key)
  105.     {
  106.       DateTime value;
  107.       if (!DateTime.TryParseExact(GetString(elm, key), "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.None, out value))
  108.         throw new Exception("<" + elm.Name + "> 元素的 " + key + " 属性的值必须为日期值");
  109.       return value;
  110.     }
  111.     bool GetBooleanFromMethod(XmlNode elm, string key)
  112.     {
  113.       var value = GetString(elm, key);
  114.       if (value == "等本息") return true;
  115.       if (value == "等本金") return false;
  116.       throw new Exception("<" + elm.Name + "> 元素的 " + key + " 属性的值必须为“等本息”或者“等本金”");
  117.     }
  118.   }
  119. }
复制代码
 
而 Pub.cs 文件中的 Pub 静态类提供的 GetMessage 方法用于显示错误信息:
  1. using System;
  2. using System.Text;
  3. namespace Skyiv.Ben.LoanCalculator
  4. {
  5.   static class Pub
  6.   {
  7.     public static string GetMessage(Exception ex)
  8.     {
  9.       var sb = new StringBuilder("错误: ");
  10.       for (var e = ex; e != null; e = e.InnerException) sb.Append(e.Message + ": ");
  11.       sb.Length -= 2;
  12.       return sb.ToString();
  13.     }
  14.   }
  15. }
复制代码
 
接着,就是 LoanBase.cs 文件中的抽象基类 LoanBase 了:
  1. using System;
  2. using System.Data;
  3. using System.Drawing;
  4. using System.Collections.Generic;
  5. namespace Skyiv.Ben.LoanCalculator
  6. {
  7.   abstract class LoanBase
  8.   {
  9.     public DataTable Table { get; private set; }
  10.     public LoanBase(decimal balance, int months, DateTime date, KeyValuePair<DateTime, PointF>[] rates)
  11.     {
  12.       Table = GetTable();
  13.       Calculate(balance, months, date, rates);
  14.     }
  15.     protected virtual void Calculate(decimal balance, int months, DateTime date, KeyValuePair<DateTime, PointF>[] rates)
  16.     {
  17.     }
  18.     protected decimal GetMonthRate(DateTime date, int months, KeyValuePair<DateTime, PointF>[] rates)
  19.     {
  20.       int i;
  21.       for (i = rates.Length - 1; i >= 0; i--) if (date >= rates[i].Key) break;
  22.       return (decimal)(months <= 60 ? rates[i].Value.X : rates[i].Value.Y) / 100 / 12;
  23.     }
  24.     protected decimal Round(decimal dec)
  25.     {
  26.       return decimal.Round(dec, 2);
  27.     }
  28.     protected DateTime NextMonth(DateTime date, int day)
  29.     {
  30.       date = date.AddMonths(1);
  31.       return (date.Day >= day) ? date : new DateTime(date.Year, date.Month,
  32.         Math.Min(day, DateTime.DaysInMonth(date.Year, date.Month)));
  33.     }
  34.     DataTable GetTable()
  35.     {
  36.       var dt = new DataTable();
  37.       dt.Columns.Add("期数", typeof(int));
  38.       dt.Columns.Add("还款日期", typeof(DateTime));
  39.       dt.Columns.Add("本金", typeof(decimal));
  40.       dt.Columns.Add("利息", typeof(decimal));
  41.       dt.Columns.Add("还款", typeof(decimal));
  42.       dt.Columns.Add("贷款余额", typeof(decimal));
  43.       dt.Columns.Add("累计还款", typeof(decimal));
  44.       dt.Columns.Add("累计利息", typeof(decimal));
  45.       return dt;
  46.     }
  47.   }
  48. }
复制代码
该类中的 Round 方法用于决定在计算时如何进行舍入,如有需要,可以修改该方法。
在该类的 GetMonthRate 方法中,根据贷款期数(months)来判断是该笔贷款是短期贷款还是中长期贷款,从而决定应该使用什么利率。
 
表示等本息法的 LoanEq 类是从 LoanBase 类中派生的:
  1. using System;
  2. using System.Drawing;
  3. using System.Collections.Generic;
  4. namespace Skyiv.Ben.LoanCalculator
  5. {
  6.   // 等本息法
  7.   sealed class LoanEq : LoanBase
  8.   {
  9.     public LoanEq(decimal balance, int months, DateTime date, KeyValuePair<DateTime, PointF>[] rates)
  10.       : base(balance, months, date, rates)
  11.     {
  12.     }
  13.     protected override void Calculate(decimal balance, int months, DateTime date, KeyValuePair<DateTime, PointF>[] rates)
  14.     {
  15.       decimal baseAmount = 0, totalAmount = 0, totalInterest = 0;
  16.       decimal monthRate0 = decimal.MinValue, monthAmount = decimal.MinValue;
  17.       for (int day = date.Day, month = months; month >= 1; month--, date = NextMonth(date, day))
  18.       {
  19.         var monthRate = GetMonthRate(date, months, rates);
  20.         var interest = Round(balance * monthRate);
  21.         if (monthRate0 != monthRate) monthAmount = GetMonthAmount(balance, monthRate0 = monthRate, month);
  22.         baseAmount = monthAmount - interest;
  23.         balance -= baseAmount;
  24.         if (month == 1 && balance != 0)
  25.         {
  26.           baseAmount += balance;
  27.           interest -= balance;
  28.           balance = 0;
  29.         }
  30.         totalAmount += monthAmount;
  31.         totalInterest += interest;
  32.         Table.Rows.Add(new object[] { months - month + 1, date, baseAmount, interest, monthAmount, balance, totalAmount, totalInterest });
  33.       }
  34.     }
  35.     decimal GetMonthAmount(decimal balance, decimal monthRate, int months)
  36.     {
  37.       double tmp = Math.Pow(1 + (double)monthRate, months);
  38.       return Round((decimal)((double)balance * (double)monthRate * tmp / (tmp - 1)));
  39.     }
  40.   }
  41. }
复制代码
在该类中覆盖了基类的 Calculate 虚方法,在主循环中逐月计算还款计划表。
等本息法在利率不变的情况下,每月的还款额是固定的,所以也称为“等额法”,计算公式如下:

月还款额 =贷款金额 x 月利率 x (1 + 月利率)期数
(1 + 月利率)期数 - 1

这个公式在 GetMonthAmount 方法中计算。
而月还利息等于上月剩余贷款余额乘以月利率,月还本金等于月还款额减去月还利息。
然后,本月剩余贷款余额等于上月剩余贷款余额减去月还本金。
最后,由于计算时需要进行舍入处理,到最后一期还款后可能剩余的贷款余额不为零,这就需要在保持月还款额不变的情况下调整月还本金和月还利息。
 
表示等本金法的 LoanDesc 类也是从 LoanBase 类中派生的:
  1. using System;
  2. using System.Drawing;
  3. using System.Collections.Generic;
  4. namespace Skyiv.Ben.LoanCalculator
  5. {
  6.   // 等本金法
  7.   class LoanDesc : LoanBase
  8.   {
  9.     public LoanDesc(decimal balance, int months, DateTime date, KeyValuePair<DateTime, PointF>[] rates)
  10.       : base(balance, months, date, rates)
  11.     {
  12.     }
  13.     protected override void Calculate(decimal balance, int months, DateTime date, KeyValuePair<DateTime, PointF>[] rates)
  14.     {
  15.       decimal baseAmount = Round(balance / months), totalAmount = 0, totalInterest = 0;
  16.       for (int day = date.Day, month = months; month >= 1; month--, date = NextMonth(date, day))
  17.       {
  18.         var monthRate = GetMonthRate(date, months, rates);
  19.         var interest = Round(balance * monthRate);
  20.         var monthAmount = baseAmount + interest;
  21.         balance -= baseAmount;
  22.         if (month == 1 && balance != 0)
  23.         {
  24.           baseAmount += balance;
  25.           monthAmount += balance;
  26.           balance = 0;
  27.         }
  28.         totalAmount += monthAmount;
  29.         totalInterest += interest;
  30.         Table.Rows.Add(new object[] { months - month + 1, date, baseAmount, interest, monthAmount, balance, totalAmount, totalInterest });
  31.       }
  32.     }
  33.   }
  34. }
复制代码
在该类中同样也覆盖了基类的 Calculate 虚方法,在主循环中逐月计算还款计划表。
等本金法的月还本金是固定的,并且在调整贷款利率时也不变,等于贷款金额除以总期数。
但是,在贷款利率不变的情况下,每月还款额却是递减的,所以也称为“递减法”。
月还利息等于上月剩余贷款余额乘以月利率,月还款额等于月还本金加上月还利息。
然后,本月剩余贷款余额等于上月剩余贷款余额减去月还本金。
最后,由于计算时需要进行舍入处理,到最后一期还款后可能剩余的贷款余额不为零,这就需要在保持月还利息不变的情况下调整月还本金和月还款额。
 
最后,MainForm.cs 文件中的 MainForm 类如下:
[code]using System;using System.Data;using System.Windows.Forms;namespace Skyiv.Ben.LoanCalculator{  public sealed partial class MainForm : Form  {    Config cfg;    public MainForm()    {      InitializeComponent();    }    private void MainForm_Load(object sender, EventArgs e)    {      btnCalculte.Enabled = false;      try      {        cfg = new Config("LoanCalculator.xml");        tbxBalance.Text = cfg.Balance.ToString();        tbxMonths.Text = cfg.Months.ToString();        dtpBegin.Value = cfg.Date;        rbnDesc.Checked = !(rbnEq.Checked = cfg.IsEq);        lbxType.DataSource = cfg.Items;        lbxType.SelectedIndex = lbxType.FindStringExact(cfg.Item);        btnCalculate.Enabled = true;      }      catch (Exception ex)      {        tbxOut.Text = Pub.GetMessage(ex);      }    }    private void lbxType_SelectedIndexChanged(object sender, EventArgs e)    {      cfg.SetRates(lbxType.SelectedValue.ToString());      dgvRate.Rows.Clear();      foreach (var kvp in cfg.Rates)        dgvRate.Rows.Add(new object[] { kvp.Key.ToString("yyyy-MM-dd"), kvp.Value.X, kvp.Value.Y });    }    private void btnCalculate_Click(object sender, EventArgs e)    {      btnCalculate.Enabled = false;      try      {        tbxOut.Text = "";        var isEq = rbnEq.Checked;        var date = dtpBegin.Value;        int months;        decimal balance;        if (!int.TryParse(tbxMonths.Text, out months) || months
您需要登录后才可以回帖 登录 | 立即注册