你真的会数钱吗?

本文已迁移至:http://thinkinside.tk/2013/01/01/money.html

快年底了,假如你们公司的美国总部给每个人发了一笔201212.21美元的特别奖金,作为程序员的你, 该如何把这笔钱收入囊中?

Table of Contents

  • 1 美元?美元!
  • 2 存入账户
  • 3 收税
  • 4 转成人民币
  • 5 分钱
  • 6 记账
  • 7 来点高级的
  • 8 其他未尽事宜
  • 9 小结

1 美元?美元!

你可能觉得,这根本不是问题。在自己的账户中直接加上一笔“转入”就行了。但是首先就遇到了币种的问题。

一般来说,银行账户都是单币种的。你可能会说不对啊,我的一卡通就能存入不同的币种啊?但那是一个“账号(Account Number)”对应的多个“账户(Account)”。 通常财务记账的时候,一个“账户(Account)”都使用同一币种。

账户(Account)记录了资金的往来,包含很多条目(Entry)。账户会记录结余,结余等于所有条目中金额的总和。

我们不可能为每个币种设计一种条目,所以需要抽象出一个货币类——Money,适用于各种不同的币种: 

 

Money类至少要记录金额和币种:

  • 对于金额,由于货币存在最小面额,所以金额的类型可以采用定点小数或者整型。考虑到会对金额进行一些运算,用整数处理应该更方便。如果用java语言实现,可以使用

lang类型。

  • 对于币种,java提供了java.util.Currency类,专门用于表示货币,符合ISO 4217货币代码标准。Currency使用Singleton模式,需要用getInstance方法获得实例。

主要的方法包括:

    • String getCurrencyCode() 获取货币的ISO 4217货币代码
    • int getDefaultFractionDigits() 获取与此货币一起使用的默认小数位数
    • static Currency getInstance(Locale locale) 返回给定语言环境的国家/地区的 Currency 实例
    • static Currency getInstance(String currencyCode) 返回给定货币代码的 Currency 实例。
    • String getSymbol() 获取默认语言环境的货币符号
    • String getSymbol(Locale locale) 获取指定语言环境的货币符号
    • String toString() 返回此货币的 ISO 4217 货币代码

通过Currency类的帮助,我们的Money类看起来大概是这个样子(为了方便,提供多种构造函数):

public class Money {private long amount;private Currency currency;public double getAmount() {return BigDecimal.valueOf(amount, currency.getDefaultFractionDigits()).doubleValue();}public Currency getCurrency() {return currency;}public Money(double amount, Currency currency) {this.currency = currency;this.amount = Math.round(amount * centFactor());}public Money(long amount, Currency currency) {this.currency = currency;this.amount = amount * centFactor();}private static final int[] cents = new int[] { 1, 10, 100, 1000,10000 };private int centFactor() {return cents[currency.getDefaultFractionDigits()];}
}

用Money类表示我们的$201212.21奖金,就是:

Money myMoney = new Money(201212.21,Currency.getInstance(Locale.US));

 

2 存入账户

终于解决了币种的问题,可以把钱存入账户了。存入的逻辑是:在条目中记录一笔账目,并计算账户的余额。

不同币种之间相加或相减是没有意义的,为了避免人为错误,在Money的代码中就要禁止这种操作。我们可以采用抛出异常的方式。 为了简单起见,这里不再定义一个单独的"MoneyException",而是直接使用java.lang.Exception:

public Money add(Money money) throws Exception{if(!money.getCurrency().equals(this.currency)){throw(new Exception("different currency can't be add"));}BigDecimal value = this.getAmount().add(money.getAmount());Money result = new Money(value.doubleValue(),this.getCurrency());return result;}public Money minus(Money money) throws Exception{if(!money.getCurrency().equals(this.currency)){throw(new Exception("different currency can't be minus"));}BigDecimal value =this.getAmount().add(money.getAmount().negate());Money result = new Money(value.doubleValue(),this.getCurrency());return result;}

 

3 收税

先不要高兴得太早,这笔钱属于“一次性所得”,需要交20%的个人所得税。税后所得应该是多少?

你可能说:是80%。只要为Money加上一个multiply(double factor)方法就可以进行计算了。

但是牵扯到了舍入的问题。由于货币存在最小单位,在做乘/除法运算的时候就要考虑到舍入的问题了。最好是能够控制舍入的行为。假如税务部门对于 舍入的计算有明确规定,我们也可以做一个遵纪守法的好公民。

在java.math.BigDecimal中定义了7种舍入模式:

  • ROUNDUP:等于远离0的数。
  • ROUNDDOWN:等于靠近0的数。
  • ROUNDCEILING:等于靠近正无穷的数。
  • ROUNDFLOOR:等于靠近负无穷的数。
  • ROUNDHALFUP:等于靠近的数,若舍入位为5,应用ROUNDUP。
  • ROUNDHALFDOWN:等于靠近的数,若舍入位为5,应用ROUNDDOWN。
  • ROUNDHALFEVEN:舍入位前一位为奇数,应用ROUNDHALFUP;舍入位前一位为偶数,应用ROUNDHALFDOWN。

我们可以借用这些模式作为参数:

public static final int ROUND_UP = BigDecimal.ROUND_UP;public static final int ROUND_DOWN = BigDecimal.ROUND_DOWN;public static final int ROUND_CEILING = BigDecimal.ROUND_CEILING;public static final int ROUND_FLOOR = BigDecimal.ROUND_FLOOR;public static final int ROUND_HALF_UP = BigDecimal.ROUND_HALF_UP;public static final int ROUND_HALF_DOWN = BigDecimal.ROUND_HALF_DOWN;public static final int ROUND_HALF_EVEN = BigDecimal.ROUND_HALF_EVEN;public static final int ROUND_UNNECESSARY = BigDecimal.ROUND_UNNECESSARY;public Money multiply(double multiplicand, int roundingMode) {BigDecimal amount = this.getAmount().multiply(new BigDecimal(multiplicand));amount = amount.divide(BigDecimal.ONE,roundingMode);return new Money(amount.doubleValue(),this.getCurrency());}public Money divide(double divisor, int roundingMode) {BigDecimal amount = this.getAmount().divide(new BigDecimal(divisor),roundingMode);Money result = new Money(amount.doubleValue(), this.getCurrency());return result;}

 

4 转成人民币

尽管各领域的国际化提了十几年,但是在国内想直接用美元消费还是有一定困难。所以你决定将这笔钱换成人民币。

对于账户来说,就是在美元账户和人民币账户分别做一笔转出和转入。 转入和转出的amount值是不同的,因为涉及到币种转换的问题。 显然,账户对象不应该知道如何进行汇率转换,责任又落在了Money类上。

最直观的做法是在Money类上增加一个convertTo(Currency currency)的方法。 但汇率实在是一个复杂的问题:

  1. 汇率是经常变化的;
  2. 汇率转换时的舍入处理会有相关的约定;

这些复杂的问题处理如果直接放在Money类上会显得十分笨重,单独设计一个MoneyConverter类会比较好:

import java.util.Currency;public interface MoneyConverter {Money convertTo(Money money,Currency currency) throws Exception;
}

 

我们实现一个最简单的转化器,使用固定的汇率值:

import java.math.BigDecimal;
import java.util.Currency;
import java.util.Locale;public class SimpleMoneyConverter implements MoneyConverter {private static final BigDecimal DOLLAR_TO_CNY =  new BigDecimal(6.2365);private static final Currency DOLLAR = Currency.getInstance(Locale.US);private static final Currency CNY = Currency.getInstance(Locale.CHINA);@Overridepublic Money convertTo(Money money,Currency target) throws Exception{if(!known(money.getCurrency()) || !known(target)){throw (new Exception("unknown currency"));}BigDecimal factorSource =BigDecimal.ONE, factorTarget = BigDecimal.ONE;if(money.getCurrency().equals(DOLLAR))factorSource = DOLLAR_TO_CNY;if(target.equals(DOLLAR))factorTarget = DOLLAR_TO_CNY;BigDecimal value = money.getAmount().multiply(factorSource).divide(factorTarget);return new Money(value.doubleValue(),target);}private boolean known(Currency currency){return(currency.equals(DOLLAR) || currency.equals(CNY) );}}

 


可以看到,即使是最简单的转换器,处理起来也比较麻烦。所以千万不要在Money类中做这件事情。

通过转换器可以很容易得到转成人民币后的值。

5 分钱

有好处不能独享。这笔钱你决定和老婆三七开。当然,你三!

这又是一个新的舍入问题:即使你指定各自的舍入计算方法,也不能保证各部分舍入后的值加总后仍等于原值。

前面的“可定制乘除法”似乎不能很好的解决这个问题,所以我们需要一个新的方法: Money[] allocate(double[] ratioes)

传入分配比例的数组,返回分配结果的数组。

为了保证分配的公平,可以使用伪随机数来处理误差。

该方法的实现如下:

public Money[] allocate(double[] ratioes) throws Exception{if(ratioes.length==0){throw (new Exception("there is no ratio"));}double ratioTotal = 0;for(double ratio:ratioes){ratioTotal += ratio;}if(0==ratioTotal){throw(new Exception("total of ratioes is zero"));}double total = this.getAmount().doubleValue();double delta = total;Money[] results = new Money[ratioes.length];for(int i=0;i<ratioes.length;i++){double amount = total*ratioes[i]/ratioTotal;results[i] = new Money(amount,this.getCurrency());delta -= results[i].getAmount().doubleValue();}int i = (int)(Math.random() * ratioes.length); results[i] = results[i].minus(new Money(delta,this.getCurrency()));return results;}

 

6 记账

将一切重要的数据保存到数据库是很通常的做法。但是将Money保存到数据库的时候,你要小心了!

Money不能作为单独的实体。如果把Money当做实体来处理,就会产生一些问题:

  1. 会有很多实体关联到Money,比如本文中的Account,Entry等。
  2. 需要非常小心处理对Money对象的引用,避免多个实体引用到同一个Money对象。在第一点的前提下,这会变得很困难。

所以应该把Money嵌入到需要的实体中,而不是把Money作为单独的实体。这样,Money仅仅是实体对象(比如Entry)的一个属性,只不过其具有多个内置的属性值。

在JPA中,可以使用@Embeddable来标注Money类。

更复杂的情况是,由于一个Account中的所有Entry都应该具有相同的Currency,将Currency保存到Account中会更简洁,Entry中只记录ammount。

可以为Money的currency属性增加@Transient标注,在Entry类的getMoney中进行组装。

7 来点高级的

在DDD(领域驱动设计)中,Money是典型的值对象(Value Object)。值对象与实体的根本区别是:值对象不需要进行标识(ID)。

这会带来一些处理上的不同:

  1. 实体对象根据ID判断是否相等,值对象只根据内部属性值判断是否相等
  2. 值对象通常小而且简单,创建的代价较小
  3. 值对象只传递值,不传递对象引用,不用判断值对象是否指向同一个物理对象
  4. 通常将值对象设计为通过构造函数进行属性设置,一旦创建就无法改变其属性值

由于值对象根据内部属性值判等,我们要为Money类覆盖equals方法: public boolean equals(Object other)

8 其他未尽事宜

  • 我们还可以为Money类增加互相比较的方法(略)
  • 可以在构造函数中进行格式校验(略)
  • 可以增加一些帮助显式的方法 使用currency的getSymbol(Locale locale)方法、和NumberFormat的format方法,比如:

    NumberFormat nf=NumberFormat.getCurrencyInstance(Locale.CHINA);

    String s=nf.format(73084.803984);// result:¥73,084.80

9 小结

本文探讨如何在应用中处理货币类型,包括币种转换、各种计算、如何持久化等内容。

货币类型是典型的值对象,本文也介绍了一点值对象的特点。更多的内容可以参考DDD。

Date: 2013-01-01 02:27:05 CST

Author: Holbrook

Org version 7.8.11 with Emacs version 24

Validate XHTML 1.0

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/266532.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Maven 系统环境变量配置

Download http://maven.apache.org/download.cgi http://mirrors.shu.edu.cn/apache/maven/maven-3/3.5.4/binaries/apache-maven-3.5.4-bin.zip 环境变量 1.添加 MAVEN_HOME&#xff1a; 变量名&#xff1a;MAVEN_HOME  变量值&#xff1a;C:\Program\apache-maven-3.5.4 注…

eclipse中文乱码解决_解决git status显示中文文件名乱码问题

使用 git status 查看本地有改动但未提交的中文文件名时&#xff0c;发现会显示为一串数字&#xff0c;没有显示中文的文件名。具体如下所示&#xff1a;$ git status# 位于分支 master# 尚未暂存以备提交的变更:# (使用 "git add ..." 更新要提交的内容)# (使用 &qu…

MongoDB 3.X 用户权限控制

摘要&#xff1a; MongoDB 3.0 安全权限访问控制&#xff0c;在添加用户上面3.0版本和之前的版本有很大的区别&#xff0c;这里就说明下3.0的添加用户的方法。 环境、测试&#xff1a; 在安装MongoDB之后&#xff0c;先关闭auth认证&#xff0c;进入查看数据库&#xff0c;只有…

java enum 变量_java枚举使用详解

package com.ljq.test;/*** 枚举用法详解** author jiqinlin**/public class TestEnum {/*** 普通枚举** author jiqinlin**/public enum ColorEnum {red, green, yellow, blue;}/*** 枚举像普通的类一样可以添加属性和方法&#xff0c;可以为它添加静态和非静态的属性或方法**…

c#小游戏_C#小游戏—钢铁侠VS太空侵略者

身为漫威迷&#xff0c;最近又把《钢铁侠》和《复仇者联盟》系列又重温了一遍&#xff0c;真的是印证了那句话&#xff1a;“读书百遍&#xff0c;其意自现”。看电影一个道理&#xff0c;每看一遍&#xff0c;都有不懂的感受~ 不知道大伙是不是也有同样的感受&#xff0c;对于…

VMware Workstation 虚拟机性能优化指南

我们经常使用虚拟机来体验各种新系统&#xff0c;做各类新鲜应用的测试&#xff0c;由于VMware Workstation虚拟机功能强大&#xff0c;又适用于最新的 Windows / linux /Sun等平台&#xff0c;而且操作方便&#xff0c;所以其普及率很高&#xff01;而 VMware Workstation 本身…

pytorch已经安装成功了为什么不能使用import_使用auto keras的过程

成功安装的总结&#xff1a;1、创建环境名为mykeras、python版本为3.6的独立环境conda create --name mykeras python3.62、用pip安装 torch0.4.1&#xff08;一定要指定版本&#xff0c;否者安装auto keras时报错&#xff09;# Python 3.6 pip install https://download.pytor…

mysql可能锁的表命令_mysql 默许是表级锁一些不太常用命令

mysql 默认是表级锁一些不太常用命令1&#xff0c;mysql默认的是表级锁。如果是启用InnoDB存储引擎那么该数据库支持行级锁。2&#xff0c;查看数据库是否支持innodb的命令SHOW variables like "have_%";另一条命令SHOW ENGINES;更为准确。查看mysql数据库的详细信息…

python制作二级菜单_Python_简单三级菜单制作

一:制作要求 1.三级菜单  2.可依次选择进入各子菜单  3.所需新知识点:字典,列表 *本文通过三种方法完成,第一种:只使用循环,第二种:使用列表,第三种:使用字典 二:FlowChart流程图与上图对应,实现方式图解:while用来判断输入的数据和允许输入数据中的哪一项匹配,if来进行判断…

html5 js

2019独角兽企业重金招聘Python工程师标准>>> http://labs.hyperandroid.com/mathmayhem 转载于:https://my.oschina.net/u/201125/blog/100934

python画图如何调整图例位置_Python-如何将图例排除在情节之外

小编典典 有很多方法可以做你想要的。要添加inalis和Navi所说的内容&#xff0c;可以使用bbox_to_anchor关键字参数将图例部分地放置在轴外and/or 减小字体大小。 在考虑减小字体大小&#xff08;这会使阅读起来非常困难&#xff09;之前&#xff0c;请尝试将图例放在不同的位置…

Haproxy 与 Cookie

1 什么是cookie ? 参考 cookie http://baike.baidu.com/view/835.htm Cookie&#xff0c;有时也用其复数形式Cookies&#xff0c;指某些网站为了辨别用户身份、进行session跟踪而储存在用户本地终端上的数据&#xff08;通常经过加密&#xff09;。定义于RFC2109&#xff0…

python绘制三维地形_【学习笔记】Python科学计算三维可视化(黄天羽、嵩天)(学习中。。)|python基础教程|python入门|python教程...

0 导学 目的&#xff1a;掌握利用三维效果表达科学和工程数据的能力 传播一种思想&#xff1a;可视化技术是数据之眼内容组织&#xff1a; 流体数据的标量可视化、矢量可视化实例 三维扫描数据&#xff08;模型/地形&#xff09;可视化实例 三维地球场景可视化实例 曲线UI交互控…

在高科技产业当中,有时候你的产品出现了问题,你反过来可以把这个问题称为你的特点...

为什么80%的码农都做不了架构师&#xff1f;>>> 在高科技产业当中&#xff0c;有时候你的产品出现了问题&#xff0c;你反过来可以把这个问题称为你的特点。刚才讲的这种“大加小”的搭配就是属于这样的例子。by高通CEO保罗雅各布 看到这句话&#xff0c;深有感触…

-9 逆序输出一个整数的各位数字_【每日算法】基础算法——归并排序[求逆序对的数量](四)(思想很经典)...

题目内容给定一个长度为n的整数数列&#xff0c;请你计算数列中的逆序对的数量。逆序对的定义如下&#xff1a;对于数列的第 i 个和第 j 个元素&#xff0c;如果满足 i < j 且 a[i] > a[j]&#xff0c;则其为一个逆序对&#xff1b;否则不是。输入格式第一行包含整数n&am…

Raphael学习笔记(2)--绘图(基本图形)

为什么80%的码农都做不了架构师&#xff1f;>>> 1、图形简介 与html5不同&#xff0c;Raphael提供了以下基本图形&#xff1a;矩形、圆形、椭圆形&#xff08;html5只有矩形&#xff09;。 Paper.rect(x,y,width,height,r)&#xff1a;绘制矩形&#xff1b; 参…

java基础57 css样式、选择器和css定位(网页知识)

本文知识点&#xff08;目录&#xff09;&#xff1a; 1、CSS样式 2、选择器 3、CSS定位 1、CSS样式 1.html 在一个网页中负责的是一个页面的结构 2.css(层叠样式表)在一个页面中负责了一个页面的样式. css文档&#xff08;教程&#xff09;&#xff1a;http://www.w3…

电子计算机场地通用规范_最全的视频监控系统施工规范要求

前言&#xff1a;今天我们一起来看下监控施工详细规范步骤。可以参考一下&#xff0c;有些东西可能不符合你的项目要求&#xff0c;但是都可以做删减正文&#xff1a;一、工程设计、验收标准《城市道路设计规范》 CJJ37-90《城市道路交通规划设计规范》GB50220-95 《道路交通标…

Intent的一些用法

2019独角兽企业重金招聘Python工程师标准>>> Intent用法实例 1.无参数Activity跳转 Java代码 Intent it new Intent(Activity.Main.this, Activity2.class); startActivity(it); 2.向下一个Activity传递数据&#xff08;使用Bundle和Intent.putExtras&#x…

idea建立一个java工程_IntelliJ IDEA(三、各种工程的创建 -- 之一 -- 创建一个简单的Java工程)...

一、创建一个简单的Java工程&#xff1a;HelloWorld1. Eclipse的第一步是选择工作空间&#xff0c;然后创建项目&#xff1b;IDEA不同(没有工作空间的概念)&#xff0c;第一步就直接创建具体的项目&#xff0c;项目创建过程中会选择在本地的存储位置(本地的某个文件夹)。Eclips…