เขียนโปรแกรม (ด้วย Go) สร้างตารางการผ่อนบ้านแบบลดต้นลดดอก
Tags: go, programming
Published: 2021-01-24 00:02:56

ช่วงนี้กำลังตัดสินใจเลือกสถาบันการเงินที่จะทำ Refinance บ้าน จริงๆก็มีหลายที่แจกตาราง Excel ให้ลองคำนวณดูว่าถ้ามีดอกเบี้ยเท่านี้ จ่ายเงินแต่ละงวดเท่านี้ จะเป็นดอกเบี้ยเท่าไหร่ ตัดเงินต้นเท่าไหร่ และ ผ่อนกี่งวดถึงจะหมด

แต่ว่าเราก็อยากทำความเข้าใจสูตรด้วยและเราเขียน script สำหรับ Excel หรือของโปรแกรม spread sheet เจ้าอื่นๆไม่เป็น ดังนั้นจึงลองมาทำความเข้าใจแล้วเขียนด้วย Go เพื่อให้เราปรับแต่งตัวแปรต่างๆได้เองง่ายๆ

ทีนี้ตอนนี้ก็เลยพยายามแกะสูตรที่ทางธนาคารใช้ กับเว็บอื่นๆใช้ พบว่าเจอสูตรคำนวณดอกเบี้ยอยู่สองแบบ

1) ธนาคารสมมติให้ชื่อว่าธนาคาร K บอกว่าถ้ามารีไฟแนนซ์ด้วยภายใน 3 ปีแรกจะให้ดอกเบี้ยที่ 1.75%, 2.20% และ MRR-1.75% (ประมาณ 4.30%) ผ่อนขั้นต่ำงวดละ 27,600 บาท ซึ่งพอแกะออกมาดูพบว่าเขาคำนวณดอกเบี้ยในแต่ละเดือนแบบนี้

> ดอกเบี้ยเดือนนี้ = เงินต้นที่เหลือจากเดือนก่อน * อัตราดอกเบี้ย / 12

ตัวอย่างเช่น ถ้ามีเงินต้นที่เหลือ 4,900,000 และอัตราดอกเบี้ย 1.75% จะได้ดอกที่ต้องจ่ายงวดที่จะถึงเป็นเงิน

> 4,900,000 * 0.0175 / 12 == 7145.83

 ปัดเศษให้เหลือทศนิยมสองตำแหน่ง เช่น 7145.833 ปัดเป็น 7145.83 ถ้าเป็น 7145.836 ปัดเป็น 7145.84

2) กับอีกสูตรที่เห็นทางผู้ให้บริการทีปรึกษาการรีไฟแนนซ์ใช้คือ

> ดอกเบี้ยเดือนนี้ = เงินต้นที่เหลือจากเดือนก่อน * อัตราดอกเบี้ย / 365 * 30

ตัวอย่างเช่น ถ้ามีเงินต้นที่เหลือ 4,900,000 และอัตราดอกเบี้ย 1.75% จะได้ดอกที่ต้องจ่ายงวดที่จะถึงเป็นเงิน

> 4,900,000 * 0.0175 / 365 * 30 == 7047.95

สิ่งที่ผมอยากได้คือโปรแกรมที่สร้างตารางที่บอกรายละเอียดแต่ละ column แบบนี้

```
เดือนที่ | วงเงินกู้คงเหลือ | อัตราดอกเบี้ย | อัตราผ่อนต่อเดือน | ชำระดอกเบี้ย | ชำระเงินต้น | เงินต้นคงเหลือ
```

ทีนี้มาดูโค้ด Go ที่ผมเขียนเอาไว้กัน เริ่มจากผมออกแบบ struct ที่ชื่อว่า Record ให้เก็บรายละเอียดของข้อมูลที่คำนวณได้แต่ละเดือนแบบนี้

```go
type Record struct {
	Month                int
	LoanSubunits         int
	MonthPaidSubunits    int
	InterestRate         float64
	InterestPaidSubunits int
	LoanPaidSubunits     int
	RestLoanSubunits     int
}
```

จะเห็นว่าค่าไหนก็ตามที่เก็บจำนวนเงิน หมดใช้ประเภทข้อมูลเป็นจำนวนเต็มหมดเลยและเก็บเป็น [subunits](https://en.wikipedia.org/wiki/Denomination_(currency)#Subunit_and_super_unit) ซึ่งเป็นหน่วยที่เล็กสุดของหน่วยค่าเงินหลัก เช่น บาท subunits ก็คือหน่วย สตางค์ ที่เราใช้ int เก็บเพราะทำให้เราไม่มีปัญหาเรื่องความแม่นยำของตัวเลขทศนิยมเหมือนที่เราใช้ [float64](https://floating-point-gui.de/) และช่วยให้เราคำนวณตอนปัดเศษง่ายอีกด้วย

ต่อไปเนื่องจากผมมีสูตรการคำนวณดอกเบี้ยถึงสองสูตร ดังนั้นจึงออกแบบเป็น type ของฟังก์ชันเพื่อให้เรากำหนดพารามิเตอร์ส่งให้กับฟังก์ชันที่เราเขียนในการสร้างตารางได้ดังนี้

```go
type interestCalcFunc func(loanSubunits int, interestRest float64) int
```

ซึ่งก็รับค่าจำนวนเงินต้นที่ต้องการคำนวณ กับอัตราดอกเบี้ยเข้าไป

ส่วนตัว 2 ฟังก์ชันที่ใช้คำนวณในแบบที่ 1 และ 2 ทำไว้แบบนี้

```go
func ksInterestCalc(loanSubunits int, interestRate float64) int {
	return int(math.Round(float64(loanSubunits) * interestRate / (12.0)))
}

func rInterestCalc(loanSubunits int, interestRest float64) int {
	return int(math.Round(float64(loanSubunits) * interestRest / 365.0 * 30))
}
```

ซึ่งก็เป็นสูตรแบบที่บอกไปแล้ว แต่น่าจะเห็นวิธีที่ผมใช้คือตอนคำนวณ เราแปลงให้เป็น float64 ก่อน หลังจากนั้นเราปัดเศษด้วย math.Round แล้วแปลงเป็น int อีกที การที่เราใช้ subunits ก็ช่วยให้เราใช้ math.Round ปัดเศษในทศนิยมตำแหน่งที่ 2 ให้เราได้โดยใช้ฟังก์ชัน math.Round ที่จะปัดเศษของทศนิยมเป็นจำนวนเต็มนั่นเอง เช่นถ้าเรามีเลข 714583.333 จากการคำนวณ ก็จะปัดเป็น 714583 ซึ่งก็คือ 7145.83 บาทนั่นเอง

ต่อไปตัวฟังก์ชันที่ใช้สร้างตารางนั้นผมทำให้มันเป็นในลักษณะของ CSV คือใช้ comma คั่น ซึ่งสะดวกในการ import ไปดูต่อในพวกโปรแกรม spread sheet ต่างๆ

ตัวโค้ดเป็นแบบนี้

```go
func printMonthlyInterest(paidPerMonth int, interestRate float64, restLoanSubunits int, from, to int, interestFn interestCalcFunc) Record {
	var r Record
	r.RestLoanSubunits = restLoanSubunits
	for i := from; i <= to; i++ {
		r = Record{
			Month:             i,
			LoanSubunits:      r.RestLoanSubunits,
                        MonthPaidSubunits: paidPerMonth,
                        InterestRate: interestRate,
		}

		r.InterestPaidSubunits = interestFn(r.LoanSubunits, interestRate)
		r.LoanPaidSubunits = r.MonthPaidSubunits - r.InterestPaidSubunits
		r.RestLoanSubunits = r.LoanSubunits - (r.MonthPaidSubunits - r.InterestPaidSubunits)

		fmt.Printf("%d, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f\n",
			r.Month,
			float64(r.LoanSubunits)/100.0,
			r.InterestRate*100.0,
			float64(r.MonthPaidSubunits)/100.0,
			float64(r.InterestPaidSubunits)/100.0,
			float64(r.LoanPaidSubunits)/100.0,
			float64(r.RestLoanSubunits)/100.0,
		)
	}
	return r
}
```

ถ้าดูจาก signature ของฟังก์ชัน

```go
func printMonthlyInterest(paidPerMonth int, interestRate float64, restLoanSubunits int, from, to int, interestFn interestCalcFunc) Record
```

จะเห็นว่ารับค่าเงินที่จะต้องจ่ายต่องวด รับค่าอัตราดอกเบี้ย รับค่าของเงินต้นที่เหลือก่อนหน้า และเลขระบุว่าจากงวดไหนถึงงวดไหน ตามด้วยฟังก์ชันในการคำนวณอัตราดอกเบี้ย

จากนั้นก็สร้างตัวแปร `var r Record` มาค่านึงเพื่อกำหนดจำนวนเงินที่เหลือของงวดก่อน ซึ่งในแต่ละรอบจำเป็นต้องเอาเงินงวดก่อนมาใช้คำนวณดังนั้นเราก็เลยต้องสร้างมาเก็บไว้ก่อน

ทีนี้ในแต่ละรอบของลูปที่วนจากงวด from ไปงวด to นั้นเราก็จะคำนวนค่าต่างๆในงวดที่ i แบบนี้

```go
r = Record{
        Month:             i,
        LoanSubunits:      r.RestLoanSubunits,
        MonthPaidSubunits: paidPerMonth,
        InterestRate: interestRate,
}

r.InterestPaidSubunits = interestFn(r.LoanSubunits, interestRate)
r.LoanPaidSubunits = r.MonthPaidSubunits - r.InterestPaidSubunits
r.RestLoanSubunits = r.LoanSubunits - r.LoanPaidSubunits
```

ดอกเบี้ยที่ต้องจ่ายก็ใช้ฟังก์ชันที่ส่งมาในการคำนวณ, เงินต้นที่จ่ายก็คือค่างวดหักดอกเบี้ย และ เงินต้นที่เหลือก็คือเงินต้นก่อนหน้า ลบเงินต้นที่หักจริงๆหลักจากตัดดอกแล้ว

ต่อไปก็เป็นแค่การแสดงผลโดยใช้ `%.2f` ในการให้แสดงทศนิยม 2 ตำแหน่ง และอะไรที่เป็น subunits อยู่เราก็จัดการหาร 100.0 ก่อน

ตัวอย่างการเอาไปใช้ถ้าเรามีเงินต้นเหลือ 4,900,000 อยากเห็นตารางการผ่อน 12 งวดแรกที่อัตราดอกเบี้ย 1.75% ก็เรียกใช้งานได้แบบนี้

```go
func main() {
	printMonthlyInterest(31_900_00, 0.0175, 4_900_000_00, 1, 12, ksInterestCalc)
}
```

ทีนี้ถ้าเราอยากคำนวณต่องวดที่ 13 ถึง 24 ก็ให้รับค่า record สุดท้ายที่รีเทิร์นกลับมาจากการเรียกครั้งแรกแล้วเอาเงินต้นที่เหลือส่งเป็นอาร์กิวเม้นให้การเรียกครั้งถัดไปเช่น

```go
func main() {
        var r Record
        r = printMonthlyInterest(31_900_00, 0.0175, 4_900_000_00, 1, 12, ksInterestCalc)
        printMonthlyInterest(31_900_00, 0.0220, r.RestLoanSubunits, 13, 24, ksInterestCalc)
}
```

ลองรันดูก็จะได้ตารางแบบนี้

```
1, 4900000.00, 1.75, 31900.00, 7145.83, 24754.17, 4875245.83
2, 4875245.83, 1.75, 31900.00, 7109.73, 24790.27, 4850455.56
3, 4850455.56, 1.75, 31900.00, 7073.58, 24826.42, 4825629.14
4, 4825629.14, 1.75, 31900.00, 7037.38, 24862.62, 4800766.52
5, 4800766.52, 1.75, 31900.00, 7001.12, 24898.88, 4775867.64
6, 4775867.64, 1.75, 31900.00, 6964.81, 24935.19, 4750932.45
7, 4750932.45, 1.75, 31900.00, 6928.44, 24971.56, 4725960.89
8, 4725960.89, 1.75, 31900.00, 6892.03, 25007.97, 4700952.92
9, 4700952.92, 1.75, 31900.00, 6855.56, 25044.44, 4675908.48
10, 4675908.48, 1.75, 31900.00, 6819.03, 25080.97, 4650827.51
11, 4650827.51, 1.75, 31900.00, 6782.46, 25117.54, 4625709.97
12, 4625709.97, 1.75, 31900.00, 6745.83, 25154.17, 4600555.80
13, 4600555.80, 2.20, 31900.00, 8434.35, 23465.65, 4577090.15
14, 4577090.15, 2.20, 31900.00, 8391.33, 23508.67, 4553581.48
15, 4553581.48, 2.20, 31900.00, 8348.23, 23551.77, 4530029.71
16, 4530029.71, 2.20, 31900.00, 8305.05, 23594.95, 4506434.76
17, 4506434.76, 2.20, 31900.00, 8261.80, 23638.20, 4482796.56
18, 4482796.56, 2.20, 31900.00, 8218.46, 23681.54, 4459115.02
19, 4459115.02, 2.20, 31900.00, 8175.04, 23724.96, 4435390.06
20, 4435390.06, 2.20, 31900.00, 8131.55, 23768.45, 4411621.61
21, 4411621.61, 2.20, 31900.00, 8087.97, 23812.03, 4387809.58
22, 4387809.58, 2.20, 31900.00, 8044.32, 23855.68, 4363953.90
23, 4363953.90, 2.20, 31900.00, 8000.58, 23899.42, 4340054.48
24, 4340054.48, 2.20, 31900.00, 7956.77, 23943.23, 4316111.25
```

เสร็จแล้วเรียบร้อยโค้ดสำหรับใช้สร้างตารางอัตราดอกเบี้ย จากนี้ก็เอาไปใช้เปลี่ยนตัวแปรต่างๆตามที่แต่ละสถาบันการเงินสามารถให้ได้ แล้วก็ตัดสินใจว่าจะรีไฟแนนซ์ไปที่ไหนคุ้มและเหมาะสมกับเราต่อไป

<a href="https://www.buymeacoffee.com/weerasak" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>