Skip to content

Commit 40a84cc

Browse files
committed
Initial commit
0 parents  commit 40a84cc

9 files changed

Lines changed: 434 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_requests:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v4
14+
15+
- name: Set up Go
16+
uses: actions/setup-go@v5
17+
with:
18+
go-version: stable
19+
20+
- name: Test
21+
run: go test ./...

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Tests
2+
*.test
3+
*.out
4+
5+
# IDEs
6+
.idea
7+
.vscode
8+
9+
# Emacs
10+
*~
11+
\#*\#
12+
13+
# OS
14+
.DS_Store

.pre-commit-config.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.6.0
4+
hooks:
5+
- id: trailing-whitespace
6+
- id: end-of-file-fixer
7+
- id: check-yaml
8+
9+
- repo: local
10+
hooks:
11+
- id: go-fmt
12+
name: go fmt
13+
entry: gofmt -w
14+
language: system
15+
files: \.go$
16+
17+
- id: go-vet
18+
name: go vet
19+
entry: go vet ./...
20+
language: system
21+
pass_filenames: false
22+
23+
- id: go-test
24+
name: go test
25+
entry: go test ./...
26+
language: system
27+
pass_filenames: false

CHANGELOG

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2026-01-14 Arian Behvandnejad <behvandnejad@gmail.com>
2+
3+
* Money: type storing values exclusively in minor units
4+
* New(minor uint64): constructor using minor units
5+
* Minor(): accessor for retrieving raw minor units
6+
* CurrencyAdapter: for conversion to major units

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Arian Behvandnejad
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# money
2+
3+
A small, strict Go package for representing monetary values safely.
4+
5+
This package stores money exclusively in *minor units* (for example
6+
cents). Its goal is not flexibility, but correctness.
7+
8+
## Design goals
9+
10+
This package takes a narrow position:
11+
12+
* Monetary values are stored as unsigned integers in minor units.
13+
* Invalid states are not representable.
14+
* Programmer errors surface promptly.
15+
16+
## Non-goals
17+
18+
This is not a full financial library.
19+
20+
* No currency metadata or exchange rates
21+
* No formatting or localization
22+
* No decimal math
23+
* No silent rounding
24+
25+
## Usage
26+
27+
Create values using minor units:
28+
29+
```go
30+
m := money.New(12345) // 123.45 in a 2-decimal currency
31+
```
32+
33+
Add and subtract safely:
34+
35+
```go
36+
a := money.New(500)
37+
b := money.New(200)
38+
39+
sum := a.Add(b) // 700
40+
diff := a.Sub(b) // 300
41+
```
42+
43+
Subtraction panics if it would go negative:
44+
45+
```go
46+
_ = money.New(100).Sub(money.New(200)) // panic
47+
```
48+
49+
Multiply using a floating-point factor. Results are always rounded down:
50+
51+
```go
52+
m := money.New(100)
53+
54+
m.Mul(1.25) // 125
55+
m.Mul(1.99) // 199
56+
```
57+
58+
Negative multipliers are rejected:
59+
60+
```go
61+
m.Mul(-0.5) // panic
62+
```
63+
64+
### Split amounts
65+
66+
Split money into N parts, distributing any remainder starting from the first part:
67+
68+
```go
69+
m := money.New(123)
70+
parts := m.Split(5) // [25, 25, 25, 24, 24]
71+
```
72+
73+
### Zero checks
74+
75+
```go
76+
money.New(0).IsZero() // true
77+
```
78+
79+
### Converting to major units
80+
81+
Conversion to major units is explicit and adapter-based:
82+
83+
```go
84+
type EUR struct{}
85+
86+
func (EUR) MinorToMajor(minor uint64) float64 {
87+
return float64(minor) / 100
88+
}
89+
90+
m := money.New(12345)
91+
eur := m.ToMajorUnits(EUR{}) // 123.45
92+
```
93+
94+
## When to use this package
95+
96+
Use it when:
97+
98+
* You need a safe internal representation for money
99+
* You want arithmetic that cannot silently go wrong
100+
* You prefer explicit failures over implicit bugs
101+
102+
Avoid it when:
103+
104+
* You need decimal math or arbitrary precision
105+
* You want permissive behavior
106+
* You allow negative monetary values
107+
* You’re modeling accounting rules directly

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/codegrapple/money
2+
3+
go 1.25.5

money.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package money
2+
3+
import "math"
4+
5+
// Money represents a monetary value stored in minor units
6+
// (for example cents) to avoid floating point errors.
7+
type Money struct {
8+
amount uint64
9+
}
10+
11+
// New creates a new Money value from minor units.
12+
func New(minor uint64) Money {
13+
return Money{amount: minor}
14+
}
15+
16+
// Minor returns the value in minor units.
17+
func (m Money) Minor() uint64 {
18+
return m.amount
19+
}
20+
21+
// Add returns a new Money with the sum of two values.
22+
func (m Money) Add(other Money) Money {
23+
return Money{
24+
amount: m.amount + other.amount,
25+
}
26+
}
27+
28+
// Sub subtracts the other value.
29+
// It panics if the result would be negative.
30+
func (m Money) Sub(other Money) Money {
31+
if other.amount > m.amount {
32+
panic("money: subtraction would result in negative value")
33+
}
34+
return Money{
35+
amount: m.amount - other.amount,
36+
}
37+
}
38+
39+
// Mul multiplies the amount by a floating-point factor.
40+
// The result is always rounded down to the nearest minor unit.
41+
func (m Money) Mul(factor float64) Money {
42+
if factor < 0 {
43+
panic("money: negative multiplier")
44+
}
45+
return Money{
46+
amount: uint64(math.Floor(float64(m.amount) * factor)),
47+
}
48+
}
49+
50+
// Split divides the money amount into n parts.
51+
// The split is exact: the sum of all parts equals the original amount.
52+
// Any remainder is distributed by adding 1 minor unit to the first parts.
53+
func (m Money) Split(n int) []Money {
54+
if n <= 0 {
55+
panic("money: split count must be positive")
56+
}
57+
58+
base := m.amount / uint64(n)
59+
rem := m.amount % uint64(n)
60+
61+
out := make([]Money, n)
62+
for i := range n {
63+
amt := base
64+
if uint64(i) < rem {
65+
amt++
66+
}
67+
out[i] = Money{amount: amt}
68+
}
69+
70+
return out
71+
}
72+
73+
// IsZero checks if the money value is zero.
74+
func (m Money) IsZero() bool {
75+
return m.amount == 0
76+
}
77+
78+
// CurrencyAdapter defines how to convert minor units
79+
// into major units for a specific currency.
80+
type CurrencyAdapter interface {
81+
MinorToMajor(minor uint64) float64
82+
}
83+
84+
// ToMajorUnits converts the money value to major units
85+
// using the provided currency adapter.
86+
func (m Money) ToMajorUnits(adapter CurrencyAdapter) float64 {
87+
return adapter.MinorToMajor(m.amount)
88+
}

0 commit comments

Comments
 (0)