github.com/shoshinnikita/budget-manager@v0.7.1-0.20220131195411-8c46ff1c6778/internal/pkg/money/money.go (about)

     1  package money
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strconv"
     7  )
     8  
     9  const precision = 100
    10  
    11  // Money is an amount with precision 2
    12  //
    13  //   - FromInt(15) -> 15
    14  //   - FromFloat(15.07) -> 15.07
    15  //   - FromFloat(-15.073) -> -15.07
    16  //   - FromFloat(15.078) -> 15.07
    17  //
    18  type Money int64
    19  
    20  // FromInt converts int64 to Money
    21  func FromInt(m int64) Money {
    22  	return Money(m * precision)
    23  }
    24  
    25  // FromFloat converts float64 to Money
    26  func FromFloat(m float64) Money {
    27  	// We can't convert float64 to int64 with precision 2 by multiplying it by 100 because we can get something
    28  	// like this: 17.83 * 100 = 1782.9999999999998
    29  	//
    30  	// We can use package 'github.com/shopspring/decimal', but it requires major refactoring.
    31  	// So, as a temporary solution, we use this algorithm:
    32  	//
    33  	// 1. Convert float64 to string with fixed precision
    34  	// 2. Remove decimal separator '.'
    35  	// 3. Parse this strings as int64
    36  	//
    37  
    38  	// Use precision 3 instead of 2 because 'AppendFloat' rounds float64
    39  	s := strconv.AppendFloat(nil, m, 'f', 3, 64)
    40  	// Replace decimal separator with the first digit and first digit with the second one: 17.830 -> 178830 -> 178330
    41  	s[len(s)-4], s[len(s)-3] = s[len(s)-3], s[len(s)-2]
    42  	// Trim last 2 digits
    43  	s = s[:len(s)-2]
    44  
    45  	res, err := strconv.ParseInt(string(s), 10, 64)
    46  	if err != nil {
    47  		// Just in case
    48  		panic(err)
    49  	}
    50  
    51  	return Money(res)
    52  }
    53  
    54  // Int converts Money to int64
    55  func (m Money) Int() int64 {
    56  	return int64(m) / precision
    57  }
    58  
    59  // Float converts Money to float64
    60  func (m Money) Float() float64 {
    61  	return float64(m) / precision
    62  }
    63  
    64  // String converts Money to string. Money is always formatted as a number with 2 digits
    65  // after decimal point (123.45, 123.00 and etc.)
    66  func (m Money) String() string {
    67  	return fmt.Sprintf("%.2f", m.Float())
    68  }
    69  
    70  // Arithmetic operations
    71  
    72  // Add returns sum of original and passed Moneys
    73  func (m Money) Add(add Money) Money {
    74  	return m + add
    75  }
    76  
    77  // Sub returns remainder after subtraction
    78  func (m Money) Sub(sub Money) Money {
    79  	return m - sub
    80  }
    81  
    82  // Div divides Money by n (if n <= 0, it panics)
    83  func (m Money) Div(n int64) Money {
    84  	if n <= 0 {
    85  		panic("n must be greater than zero")
    86  	}
    87  
    88  	// Don't use Money.ToInt for better precision
    89  	money := int64(m)
    90  	return Money(money / n)
    91  }
    92  
    93  // Other
    94  
    95  // Round is like 'math.Round'
    96  func (m Money) Round() Money {
    97  	if m == 0 {
    98  		return m
    99  	}
   100  
   101  	mod := m % precision
   102  	if mod == 0 {
   103  		return m
   104  	}
   105  
   106  	m -= mod
   107  	switch {
   108  	case mod >= 50:
   109  		m += precision
   110  	case mod <= -50:
   111  		m -= precision
   112  	}
   113  
   114  	return m
   115  }
   116  
   117  // Ceil is like 'math.Ceil'
   118  func (m Money) Ceil() Money {
   119  	if m == 0 {
   120  		return m
   121  	}
   122  
   123  	mod := m % precision
   124  	if mod == 0 {
   125  		return m
   126  	}
   127  
   128  	m -= mod
   129  	if m > 0 {
   130  		m += precision
   131  	}
   132  	return m
   133  }
   134  
   135  // Floor is like 'math.Floor'
   136  func (m Money) Floor() Money {
   137  	if m == 0 {
   138  		return m
   139  	}
   140  
   141  	mod := m % precision
   142  	if mod == 0 {
   143  		return m
   144  	}
   145  
   146  	m -= mod
   147  	if m < 0 {
   148  		m -= precision
   149  	}
   150  	return m
   151  }
   152  
   153  // Encoding and Decoding
   154  
   155  var (
   156  	_ json.Marshaler   = (*Money)(nil)
   157  	_ json.Unmarshaler = (*Money)(nil)
   158  )
   159  
   160  func (m Money) MarshalJSON() ([]byte, error) {
   161  	// Always format with 2 digits after decimal point (123.45, 123.00 and etc.)
   162  	return []byte(m.String()), nil
   163  }
   164  
   165  func (m *Money) UnmarshalJSON(data []byte) error {
   166  	f, err := strconv.ParseFloat(string(data), 64)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	*m = FromFloat(f)
   171  	return nil
   172  }
   173  
   174  var _ fmt.Formatter = (*Money)(nil)
   175  
   176  // Format implements 'fmt.Formatter' interface. It divides a number in groups of three didits
   177  // separated by thin space
   178  func (m Money) Format(f fmt.State, c rune) {
   179  	const thinSpace = " "
   180  
   181  	str := m.String()
   182  
   183  	switch c {
   184  	case 'd':
   185  		str = str[:len(str)-3]
   186  		if str == "-0" {
   187  			str = "0"
   188  		}
   189  	case 'f':
   190  		// Do nothing
   191  	default:
   192  		var negative bool
   193  		// There's a case when minus is separated by thin space (- 100 000.00).
   194  		// So, trim it for a while.
   195  		if str[0] == '-' {
   196  			negative = true
   197  			str = str[1:]
   198  		}
   199  
   200  		// This algorithm can be buggy because the string is changing in process, but
   201  		// it works for 1000000000000.00 (one trillion must be enough for all cases) and
   202  		// it is very simple. So, leave it as is.
   203  
   204  		for i := len(str) - 6; i > 0; i -= 3 {
   205  			// We don't use comma as a separator because:
   206  			//
   207  			//   The 22nd General Conference on Weights and Measures declared in 2003 that
   208  			//   "the symbol for the decimal marker shall be either the point on the line or
   209  			//   the comma on the line". It further reaffirmed that "numbers may be divided in
   210  			//   groups of three in order to facilitate reading; neither dots nor commas are ever
   211  			//   inserted in the spaces between groups"
   212  			//
   213  			// Source: https://en.wikipedia.org/wiki/Decimal_separator#Current_standards
   214  			//
   215  			// Use thin space ' ' instead (https://en.wikipedia.org/wiki/Thin_space)
   216  
   217  			str = str[:i] + thinSpace + str[i:]
   218  		}
   219  
   220  		if negative {
   221  			str = "-" + str
   222  		}
   223  	}
   224  
   225  	f.Write([]byte(str))
   226  }