现在,很多人都有个人住房贷款,或者将要有个人住房贷款。那么,就让我们用 C# 写一个计算器,用于计算个人住房贷款的还款计划表。
这个计算器能够根据你给出的贷款金额、贷款期数、贷款日期、还款方式、贷款种类,计算出相应的还款计划表,如上图所示。
这样,就很容易知道每月要还多少钱,到现在为止剩余多少贷款未还,最终要付出多少贷款利息,等等。
贷款利率是由贷款种类决定的,存放在 LoanCalculator.xml 文件中:
<?xml version="1.0" encoding="utf-8" ?> <LoanCalculator> <option balance="13.8" months="180" date="2004-07-23" method="等本息" item="公积金" /> <items> <item title="公积金"> <rate date="1001-01-01" low="3.6" high="4.05" /> <rate date="2005-01-01" low="3.78" high="4.23" /> <rate date="2006-01-01" low="3.96" high="4.41" /> <rate date="2007-01-01" low="4.14" high="4.59" /> <rate date="2008-01-01" low="5.04" high="5.22" /> <rate date="2009-01-01" low="3.33" high="3.87" /> </item> <item title="商业性基准"> <rate date="1001-01-01" low="5.51" high="5.75" /> <rate date="2007-01-01" low="5.51" high="5.81" /> <rate date="2008-01-01" low="6.579" high="6.65" /> <rate date="2009-01-01" low="5.76" high="5.94" /> </item> <item title="商业性优惠"> <rate date="1001-01-01" low="5.51" high="5.75" /> <rate date="2007-01-01" low="5.51" high="5.81" /> <rate date="2008-01-01" low="6.579" high="6.65" /> <rate date="2009-01-01" low="4.03" high="4.16" /> </item> </items> </LoanCalculator>
你可以自行修改这个文件,以适应不同银行的贷款利率。
这个文件由 Config.cs 文件中的 Config 类读取:
using System; using System.Xml; using System.Drawing; using System.Collections.Generic; namespace Skyiv.Ben.LoanCalculator { sealed class Config { static readonly string ElmOption = "option"; static readonly string ElmItems = "items"; static readonly string AttrBalance = "balance"; static readonly string AttrMonths = "months"; static readonly string AttrDate = "date"; static readonly string AttrMethod = "method"; static readonly string AttrItem = "item"; static readonly string AttrTitle = "title"; static readonly string AttrLow = "low"; static readonly string AttrHigh = "high"; public decimal Balance { get; private set; } // 贷款金额(万元) public int Months { get; private set; } // 贷款期数(月) public DateTime Date { get; private set; } // 贷款日期 public bool IsEq { get; private set; } // 还款方式: true:等本息 false: 等本金 public string Item { get; private set; } // 贷款种类 public string[] Items { get; private set; } // 贷款种类列表 public KeyValuePair<DateTime, PointF>[] Rates { get; private set; } // 贷款利率 KeyValuePair<DateTime, PointF>[][] ratesArray; // 各种类的“贷款利率”列表 public Config(string fileName) { try { var doc = new XmlDocument(); doc.Load(fileName); var elm = doc.DocumentElement[ElmOption]; if (elm == null) throw new Exception("未能找到 <" + ElmOption + "> 元素"); Balance = GetDecimal(elm, AttrBalance); Months = GetInt32(elm, AttrMonths); Date = GetDateTime(elm, AttrDate); IsEq = GetBooleanFromMethod(elm, AttrMethod); Item = GetString(elm, AttrItem); Items = GetItemsAndLoadRatesArray(doc); SetRates(Item); } catch (Exception ex) { throw new Exception("读配置文件(" + fileName + ")", ex); } } // 根据贷款种类设置贷款利率 public void SetRates(string key) { var idx = Array.IndexOf(Items, key); if (idx < 0) throw new Exception("无此贷款种类: " + key); Rates = ratesArray[idx]; } string[] GetItemsAndLoadRatesArray(XmlDocument doc) { var elm = doc.DocumentElement[ElmItems]; if (elm == null) throw new Exception("未能找到 <" + ElmItems + "> 元素"); var elms = elm.ChildNodes; var items = new string[elms.Count]; ratesArray = new KeyValuePair<DateTime, PointF>[elms.Count][]; for (var i = 0; i < elms.Count; i++) { items[i] = GetString(elms[i], AttrTitle); ratesArray[i] = GetRates(elms[i]); } return items; } KeyValuePair<DateTime, PointF>[] GetRates(XmlNode elm) { var elms = elm.ChildNodes; var rates = new KeyValuePair<DateTime, PointF>[elms.Count]; for (var i = 0; i < elms.Count; i++) rates[i] = new KeyValuePair<DateTime, PointF>(GetDateTime(elms[i], AttrDate), new PointF(GetFloat(elms[i], AttrLow), GetFloat(elms[i], AttrHigh))); return rates; } string GetString(XmlNode elm, string key) { if (elm.Attributes[key] == null) throw new Exception("未能找到 <" + elm.Name + "> 元素的 " + key + " 属性"); return elm.Attributes[key].Value; } float GetFloat(XmlNode elm, string key) { float value; if (!float.TryParse(GetString(elm, key), out value)) throw new Exception("<" + elm.Name + "> 元素的 " + key + " 属性的值必须为浮点数"); return value; } decimal GetDecimal(XmlNode elm, string key) { decimal value; if (!decimal.TryParse(GetString(elm, key), out value)) throw new Exception("<" + elm.Name + "> 元素的 " + key + " 属性的值必须为实数"); return value; } int GetInt32(XmlNode elm, string key) { int value; if (!int.TryParse(GetString(elm, key), out value)) throw new Exception("<" + elm.Name + "> 元素的 " + key + " 属性的值必须为整数"); return value; } DateTime GetDateTime(XmlNode elm, string key) { DateTime value; if (!DateTime.TryParseExact(GetString(elm, key), "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.None, out value)) throw new Exception("<" + elm.Name + "> 元素的 " + key + " 属性的值必须为日期值"); return value; } bool GetBooleanFromMethod(XmlNode elm, string key) { var value = GetString(elm, key); if (value == "等本息") return true; if (value == "等本金") return false; throw new Exception("<" + elm.Name + "> 元素的 " + key + " 属性的值必须为“等本息”或者“等本金”"); } } }
而 Pub.cs 文件中的 Pub 静态类提供的 GetMessage 方法用于显示错误信息:
using System; using System.Text; namespace Skyiv.Ben.LoanCalculator { static class Pub { public static string GetMessage(Exception ex) { var sb = new StringBuilder("错误: "); for (var e = ex; e != null; e = e.InnerException) sb.Append(e.Message + ": "); sb.Length -= 2; return sb.ToString(); } } }
接着,就是 LoanBase.cs 文件中的抽象基类 LoanBase 了:
using System; using System.Data; using System.Drawing; using System.Collections.Generic; namespace Skyiv.Ben.LoanCalculator { abstract class LoanBase { public DataTable Table { get; private set; } public LoanBase(decimal balance, int months, DateTime date, KeyValuePair<DateTime, PointF>[] rates) { Table = GetTable(); Calculate(balance, months, date, rates); } protected virtual void Calculate(decimal balance, int months, DateTime date, KeyValuePair<DateTime, PointF>[] rates) { } protected decimal GetMonthRate(DateTime date, int months, KeyValuePair<DateTime, PointF>[] rates) { int i; for (i = rates.Length - 1; i >= 0; i--) if (date >= rates[i].Key) break; return (decimal)(months <= 60 ? rates[i].Value.X : rates[i].Value.Y) / 100 / 12; } protected decimal Round(decimal dec) { return decimal.Round(dec, 2); } protected DateTime NextMonth(DateTime date, int day) { date = date.AddMonths(1); return (date.Day >= day) ? date : new DateTime(date.Year, date.Month, Math.Min(day, DateTime.DaysInMonth(date.Year, date.Month))); } DataTable GetTable() { var dt = new DataTable(); dt.Columns.Add("期数", typeof(int)); dt.Columns.Add("还款日期", typeof(DateTime)); dt.Columns.Add("本金", typeof(decimal)); dt.Columns.Add("利息", typeof(decimal)); dt.Columns.Add("还款", typeof(decimal)); dt.Columns.Add("贷款余额", typeof(decimal)); dt.Columns.Add("累计还款", typeof(decimal)); dt.Columns.Add("累计利息", typeof(decimal)); return dt; } } }
该类中的 Round 方法用于决定在计算时如何进行舍入,如有需要,可以修改该方法。
在该类的 GetMonthRate 方法中,根据贷款期数(months)来判断是该笔贷款是短期贷款还是中长期贷款,从而决定应该使用什么利率。
表示等本息法的 LoanEq 类是从 LoanBase 类中派生的:
using System; using System.Drawing; using System.Collections.Generic; namespace Skyiv.Ben.LoanCalculator { // 等本息法 sealed class LoanEq : LoanBase { public LoanEq(decimal balance, int months, DateTime date, KeyValuePair<DateTime, PointF>[] rates) : base(balance, months, date, rates) { } protected override void Calculate(decimal balance, int months, DateTime date, KeyValuePair<DateTime, PointF>[] rates) { decimal baseAmount = 0, totalAmount = 0, totalInterest = 0; decimal monthRate0 = decimal.MinValue, monthAmount = decimal.MinValue; for (int day = date.Day, month = months; month >= 1; month--, date = NextMonth(date, day)) { var monthRate = GetMonthRate(date, months, rates); var interest = Round(balance * monthRate); if (monthRate0 != monthRate) monthAmount = GetMonthAmount(balance, monthRate0 = monthRate, month); baseAmount = monthAmount - interest; balance -= baseAmount; if (month == 1 && balance != 0) { baseAmount += balance; interest -= balance; balance = 0; } totalAmount += monthAmount; totalInterest += interest; Table.Rows.Add(new object[] { months - month + 1, date, baseAmount, interest, monthAmount, balance, totalAmount, totalInterest }); } } decimal GetMonthAmount(decimal balance, decimal monthRate, int months) { double tmp = Math.Pow(1 + (double)monthRate, months); return Round((decimal)((double)balance * (double)monthRate * tmp / (tmp - 1))); } } }
在该类中覆盖了基类的 Calculate 虚方法,在主循环中逐月计算还款计划表。
等本息法在利率不变的情况下,每月的还款额是固定的,所以也称为“等额法”,计算公式如下:
月还款额 = 贷款金额 x 月利率 x (1 + 月利率)期数
(1 + 月利率)期数 - 1
这个公式在 GetMonthAmount 方法中计算。
而月还利息等于上月剩余贷款余额乘以月利率,月还本金等于月还款额减去月还利息。
然后,本月剩余贷款余额等于上月剩余贷款余额减去月还本金。
最后,由于计算时需要进行舍入处理,到最后一期还款后可能剩余的贷款余额不为零,这就需要在保持月还款额不变的情况下调整月还本金和月还利息。
表示等本金法的 LoanDesc 类也是从 LoanBase 类中派生的:
using System; using System.Drawing; using System.Collections.Generic; namespace Skyiv.Ben.LoanCalculator { // 等本金法 class LoanDesc : LoanBase { public LoanDesc(decimal balance, int months, DateTime date, KeyValuePair<DateTime, PointF>[] rates) : base(balance, months, date, rates) { } protected override void Calculate(decimal balance, int months, DateTime date, KeyValuePair<DateTime, PointF>[] rates) { decimal baseAmount = Round(balance / months), totalAmount = 0, totalInterest = 0; for (int day = date.Day, month = months; month >= 1; month--, date = NextMonth(date, day)) { var monthRate = GetMonthRate(date, months, rates); var interest = Round(balance * monthRate); var monthAmount = baseAmount + interest; balance -= baseAmount; if (month == 1 && balance != 0) { baseAmount += balance; monthAmount += balance; balance = 0; } totalAmount += monthAmount; totalInterest += interest; Table.Rows.Add(new object[] { months - month + 1, date, baseAmount, interest, monthAmount, balance, totalAmount, totalInterest }); } } } }
在该类中同样也覆盖了基类的 Calculate 虚方法,在主循环中逐月计算还款计划表。
等本金法的月还本金是固定的,并且在调整贷款利率时也不变,等于贷款金额除以总期数。
但是,在贷款利率不变的情况下,每月还款额却是递减的,所以也称为“递减法”。
月还利息等于上月剩余贷款余额乘以月利率,月还款额等于月还本金加上月还利息。
然后,本月剩余贷款余额等于上月剩余贷款余额减去月还本金。
最后,由于计算时需要进行舍入处理,到最后一期还款后可能剩余的贷款余额不为零,这就需要在保持月还利息不变的情况下调整月还本金和月还款额。
最后,MainForm.cs 文件中的 MainForm 类如下:
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 <= 0) throw new Exception("贷款期数必须是正整数"); if (!decimal.TryParse(tbxBalance.Text, out balance) || balance <= 0) throw new Exception("贷款金额必须是正数"); balance *= 10000; // 贷款金额单位是万元 var loan = isEq ? (new LoanEq(balance, months, date, cfg.Rates) as LoanBase) : (new LoanDesc(balance, months, date, cfg.Rates) as LoanBase); dgvOut.Rows.Clear(); foreach (DataRow row in loan.Table.Rows) dgvOut.Rows.Add(row.ItemArray); } catch (Exception ex) { tbxOut.Text = Pub.GetMessage(ex); } btnCalculate.Enabled = true; } } }
当用户改变贷款种类时,调用该类的 lbxType_SelectedIndexChanged 方法来相应改变贷款利率。
当用户点击“计算”按钮时,就调用该类的 btnCalculate_Click 方法来计算还款计划表。
个人住房贷款计算器以及全部的源程序,可以在这里下载。