indianfinance

package module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 2 Imported by: 0

README

indian-finance-formulas-go

Zero-dependency finance formulas for India — EMI, prepayment, SIP, step-up SIP, GST, gratuity, income tax. Go port of indian-finance-formulas.

go get github.com/javeed450-sudo/indian-finance-formulas-go
import indianfinance "github.com/javeed450-sudo/indian-finance-formulas-go"

emi := indianfinance.EMI(3000000, 8.5, 240) // 26034.70

Why it exists

Every function is extracted from the calculators running at emicalcs.com. The point of publishing them is that a finance formula should be checkable, not taken on trust.

The test suite is a direct port of the JavaScript one — same 22 cases, same expected values, same tolerances — so both implementations are held to a single standard rather than drifting into two.

go test ./...

Statutory values are options, not constants

Tax slabs, the gratuity ceiling and the GST interest rate change by notification. Every one of them is a documented option with a default, so you can update it the day it moves instead of waiting for a release:

// Central Government civil employees: Rs 25,00,000 ceiling
indianfinance.ComputeGratuity(500000, 30, indianfinance.GratuityOptions{Ceiling: 2500000})

// your own slabs when they change
indianfinance.IncomeTaxNewRegime(1600000, indianfinance.TaxOptions{Slabs: mySlabs})

Three things this gets right that most implementations do not

GST interest runs on the cash-ledger portion. Rule 88B(1) charges interest on tax actually paid in cash, not on the gross output liability. Running it on the gross bill overstates the interest — on the worked example in the tests, by 3.3×.

The 87A rebate carries marginal relief. Tax cannot exceed the income above the rebate threshold. Without it, earning one rupee more than ₹12,00,000 would cost more than one rupee.

A step-up SIP does not beat a flat SIP of the same total outlay. It wins in headline terms only because more money goes in. Hold the money constant and the flat schedule wins, because its rupees compound for longer. That is asserted in the test suite, not just claimed here.

An SWP survives or fails on whether you index the withdrawal to inflation, and most SWP calculators do not offer that input at all. A ₹50,00,000 corpus drawing ₹30,000/month at 8% finishes 20 years with ₹68,45,597 still in it. Index that same ₹30,000 to 6% and it runs dry after 17 years 1 month — nearly three years short, on identical corpus, return and starting withdrawal.

Tenure ceiling

Schedule functions step one period at a time, so an absurd tenure is an out-of-memory crash in the caller's process rather than a slow answer. Tenure is capped at 1200 months / 100 years — far beyond the longest real home loan — and exceeding it returns an *ErrTenure instead.

Licence

MIT. Not financial advice.

Documentation

Overview

Package indianfinance implements zero-dependency finance formulas for India — loans, investments, GST, gratuity and income tax.

Every function here is extracted from the calculators running at https://emicalcs.com and is covered by the test suite in finance_test.go, which is a direct port of the JavaScript suite the same formulas ship with.

Statutory figures — tax slabs, gratuity ceilings, GST rates — change by notification. Where a function depends on one, the value is a documented OPTION with a default rather than a hidden constant, so you can update it yourself the day it moves without waiting for a release.

A note on types

The JavaScript original takes months and years as numbers and rounds them. This port takes them as int where the underlying loop is integer-stepped, which removes the NaN and Infinity cases the JS version has to guard against at runtime. The tenure ceiling below is kept regardless.

MIT licensed.

Index

Constants

View Source
const (
	MaxMonths = 1200
	MaxYears  = MaxMonths / 12
)

Every schedule function below steps one period at a time, so the loop is bounded only by the tenure it is handed. Left unchecked, a bad tenure is an out-of-memory crash in the CALLER's process — not a slow answer — and there is no UI here to clamp the input first. 1200 months / 100 years is far beyond anything real (the longest home loan on offer runs 30-40 years), so the ceiling only ever fires on input that was never going to mean anything.

View Source
const DefaultGSTInterestRate = 18.0

DefaultGSTInterestRate is the notified annual rate under Section 50(1).

Variables

This section is empty.

Functions

func AnnualContributionMaturity

func AnnualContributionMaturity(yearlyAmount, annualRatePct float64, years int) (float64, error)

AnnualContributionMaturity returns the maturity of an annual-contribution scheme such as PPF or SSY. It returns an *ErrTenure if years exceeds MaxYears.

func CAGR

func CAGR(initial, final, years float64) float64

CAGR returns the compound annual growth rate as a percentage.

func EMI

func EMI(principal, annualRate float64, months int) float64

EMI returns the reducing-balance monthly instalment. annualRate is a percentage, e.g. 8.5 for 8.5% p.a.

func GSTInterest

func GSTInterest(cashTaxPaid float64, days int, annualRatePct float64) float64

GSTInterest returns interest on a late GST payment.

Under Rule 88B(1) interest runs on the tax actually paid in cash from the electronic cash ledger — NOT on the gross output liability. Getting this wrong overstates the interest, often by several times. Pass cashTaxPaid as the cash-ledger portion, not the gross bill.

Pass annualRatePct as DefaultGSTInterestRate unless the notified rate has moved. The two calculations side by side, cash-ledger against gross bill: https://emicalcs.com/gst-interest-calculator/

func Lumpsum

func Lumpsum(principal, annualRate, years float64) float64

Lumpsum returns the future value of a one-off investment.

func SIPFutureValue

func SIPFutureValue(monthly, annualRate float64, months int) float64

SIPFutureValue returns the future value of a monthly SIP, with the contribution made at the start of each month.

func TotalInterest

func TotalInterest(principal, annualRate float64, months int) float64

TotalInterest returns the total interest paid over the full tenure.

Types

type AmortRow

type AmortRow struct {
	Month     int
	Opening   float64
	Interest  float64
	Principal float64
	Closing   float64
}

AmortRow is one month of an amortisation schedule.

func Amortisation

func Amortisation(principal, annualRate float64, months int) ([]AmortRow, error)

Amortisation returns the month-by-month schedule. It returns an *ErrTenure if months exceeds MaxMonths.

type ErrTenure

type ErrTenure struct {
	Unit  string
	Got   int
	Limit int
}

ErrTenure is returned when a tenure exceeds the ceiling. Use errors.Is to match it; the wrapped message carries the offending value.

func (*ErrTenure) Error

func (e *ErrTenure) Error() string

type GST

type GST struct {
	Base  float64
	Tax   float64
	Total float64
}

GST is a base amount, its tax and the gross total.

func GSTAdd

func GSTAdd(amount, ratePct float64) GST

GSTAdd adds GST to a base amount. Interactive version: https://emicalcs.com/gst-calculator/

func GSTRemove

func GSTRemove(grossAmount, ratePct float64) GST

GSTRemove strips GST out of a gross (GST-inclusive) amount.

type Gratuity

type Gratuity struct {
	Eligible     bool
	Amount       float64
	FormulaValue float64
	Ceiling      float64
	Capped       bool
}

Gratuity is the computed entitlement.

func ComputeGratuity

func ComputeGratuity(lastSalary, years float64, opts GratuityOptions) Gratuity

ComputeGratuity applies the Code on Social Security, 2020 (in force 21 Nov 2025), whose text is published at https://labour.gov.in lastSalary is the last drawn monthly Basic + DA; years is completed years of service.

type GratuityOptions

type GratuityOptions struct {
	Ceiling  float64 // 0 -> 20,00,000. Use 25,00,000 for Central Government civil employees.
	MinYears float64 // 0 -> 5. Use 1 for fixed-term employees.
}

GratuityOptions carries the statutory figures that change by notification. The zero value means "use the defaults".

type PrepaymentInput

type PrepaymentInput struct {
	Principal      float64
	AnnualRate     float64
	Months         int // original tenure
	MonthlyExtra   float64
	LumpSum        float64
	LumpSumAtMonth int // 0 = before the first instalment
}

PrepaymentInput describes a prepayment scenario. The EMI is held fixed, so prepaying shortens the tenure rather than reducing the instalment.

type PrepaymentResult

type PrepaymentResult struct {
	InterestSaved float64
	MonthsSaved   int
	NewMonths     int
	NewInterest   float64
}

PrepaymentResult is what prepaying achieves.

func Prepayment

func Prepayment(in PrepaymentInput) (PrepaymentResult, error)

Prepayment computes the effect of prepaying with the EMI held fixed, so the tenure shrinks rather than the instalment.

See the lever plotted against tenure at https://emicalcs.com/home-loan-prepayment-calculator/

It returns an *ErrTenure if Months exceeds MaxMonths.

type SWPInput added in v1.1.0

type SWPInput struct {
	Corpus            float64
	MonthlyWithdrawal float64
	AnnualRatePct     float64
	Years             int
	// AnnualIncreasePct raises the withdrawal on each anniversary, so its buying
	// power holds against inflation. 0 is a flat SWP. Indian retirees typically
	// use 5-6%.
	AnnualIncreasePct float64
}

SWPInput describes a systematic withdrawal plan.

type SWPResult added in v1.1.0

type SWPResult struct {
	FinalBalance   float64
	TotalWithdrawn float64
	Gains          float64
	// DepletedAtMonth is the month the corpus ran dry, or 0 if it survived the
	// full period.
	DepletedAtMonth int
}

SWPResult is the outcome of a systematic withdrawal plan.

func SWP added in v1.1.0

func SWP(in SWPInput) (SWPResult, error)

SWP models a systematic withdrawal plan: money is taken out at the start of each month and the remainder stays invested, compounding monthly.

The variable that decides the outcome is AnnualIncreasePct, and most SWP calculators do not offer it. Worked example, from the test suite: a Rs 50,00,000 corpus withdrawing Rs 30,000 a month at 8% finishes 20 years with Rs 68,45,597 still in it — the corpus outgrows the withdrawals. Index that same Rs 30,000 to 6% inflation and it runs dry after 17 years 1 month. Same corpus, same return, same starting withdrawal, nearly three years of retirement gone.

Run your own numbers at https://emicalcs.com/swp-calculator/

It returns an *ErrTenure if Years exceeds MaxYears.

type Slab

type Slab struct {
	Upto float64
	Rate float64
}

Slab is one income-tax bracket: everything up to Upto is taxed at Rate. The final slab must use math.Inf(1) as Upto.

func DefaultSlabs

func DefaultSlabs() []Slab

DefaultSlabs are the new-regime slabs for FY 2026-27 (AY 2027-28), unchanged by Budget 2026.

type StepUpResult

type StepUpResult struct {
	FutureValue float64
	Invested    float64
	Gain        float64
}

StepUpResult is the outcome of a step-up SIP.

func StepUpSIP

func StepUpSIP(monthly, annualRate float64, years int, stepUpPct float64) (StepUpResult, error)

StepUpSIP models a SIP whose contribution rises by stepUpPct every 12 months.

Worth knowing: a step-up SIP does NOT beat a flat SIP of the same total outlay. It wins in headline terms only because more money goes in. Hold the money constant and the flat schedule wins, because its rupees compound for longer. See https://emicalcs.com/step-up-sip-calculator/

It returns an *ErrTenure if years exceeds MaxYears.

type Tax

type Tax struct {
	Tax   float64
	Cess  float64
	Total float64
}

Tax is the computed liability.

func IncomeTaxNewRegime

func IncomeTaxNewRegime(taxableIncome float64, opts TaxOptions) Tax

IncomeTaxNewRegime computes tax under Section 115BAC, including the Section 87A rebate with marginal relief — tax cannot exceed the income above the rebate threshold, otherwise earning one rupee more would cost more than one rupee. That relief band is easiest to see plotted: https://emicalcs.com/income-tax-calculator/

type TaxOptions

type TaxOptions struct {
	Slabs      []Slab  // nil -> DefaultSlabs
	RebateUpto float64 // 0 -> 12,00,000 (Section 87A)
	Cess       float64 // 0 -> 0.04
}

TaxOptions overrides the statutory defaults. The zero value means "use the FY 2026-27 defaults".

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL