你真的会数钱吗?

本文已迁移至: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 注…

全国计算机等级考试题库二级C操作题100套(第19套)

第19套&#xff1a; 程序通过定义学生结构体变量&#xff0c;存储了学生的学号、姓名和3门课的成绩。函数fun的功能是将形参a所指结构体变量s中的数据进行修改,并把a中地址作为函数值返回主函数&#xff0c;在主函数中输出修改后的数据。 例如&#xff1a;a所指变量s中的学号…

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;只有…

oracle和sql server取第一条记录的区别以及rownum详解

我们知道学生可能有重名的情况&#xff0c;那么当重名的时候假设只需要取得重名结果集中的第一条记录。 sql server:select top(1) num,Name from M_Student where name xy Oracle:select num,Name from M_Student where name xy and rownum < 1 对于rownum在oracle的使用…

全国计算机等级考试题库二级C操作题100套(第20套)

第20套&#xff1a; 给定程序中&#xff0c;函数fun的功能是&#xff1a;计算形参x所指数组中N个数的平均值&#xff08;规定所有数均为正数&#xff09;,将所指数组中小于平均值的数据移至数组的前部&#xff0c;大于等于平均值的数据移至x所指数组的后部&#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;对于…

输入年份月份判断是平年还是闰年及这个月有多少天

import java.util.Scanner;public class year {public static void main(String[] args) {System.out.println("请输入年份");Scanner sc new Scanner(System.in);int s sc.nextInt();System.out.println("请输入月份");Scanner sc1 new Scanner(System…

VMware Workstation 虚拟机性能优化指南

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

全国计算机等级考试题库二级C操作题100套(第21套)

第21套&#xff1a; 给定程序中&#xff0c;函数fun的功能是&#xff1a;计算形参x所指数组中N个数的平均值&#xff08;规定所有数均为正数&#xff09;,将所指数组中大于平均值的数据移至数组的前部&#xff0c;小于等于平 均值的数据移至x所指数组的后部&#xff0c;平均值…

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…

mysql5.0 php_php怎么连接mysql5.0?

PHP与MySQL的连接有三种API接口&#xff0c;分别是&#xff1a;PHP的MySQL扩展 、PHP的mysqli扩展 、PHP数据对象(PDO) &#xff0c;下面针对以上三种连接方式做下总结&#xff0c;以备在不同场景下选出最优方案。PHP的MySQL扩展是设计开发允许php应用与MySQL数据库交互的早期扩…

Android----Google code android开源项目(四)

Google code android开源项目(四) 1. sparserss http://code.google.com/p/sparserss/ 一个简洁的android rss阅读器 2. desktopsms http://code.google.com/p/desktopsms/ 在电脑写短信 3. kraigsandroid http://code.google.com/p/kraigsandroid/ 创建者…

面试题之--实现取余

def quyu(shu,chushu):if shuchushu:print(余数是0)elif shu<chushu:print(余数是&#xff1a;%s%shu)elif chushu0:print(除数不能为0)else:for i in range(0,shu):if shu-i*chushu>chushu:i1elif shu-i*chushuchushu:print(余数是0)breakelif shu-i*chushu0:print(余数…

全国计算机等级考试题库二级C操作题100套(第22套)

第22套&#xff1a; 给定程序中&#xff0c;函数fun的功能是&#xff1a;将自然数1&#xff5e;10以及它们的平方根写到名为myfile3.txt的文本文件中&#xff0c;然后再顺序读出显示在屏幕上。 请在程序的下划线处填入正确的内容并把下划线删除&#xff0c;使程序得出正确的结…

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来进行判断…

RSA使用简述

RSA协议我不再描述&#xff0c;大家可以看http://www.di-mgt.com.au/rsa_alg.html。 RSA的密钥对生成时间依赖于两个因素&#xff0c; 第一&#xff0c;密钥的长度 第二&#xff0c;素数的筛选质量 在整个密钥对生成过程中&#xff0c;RSA会随机选择两个大素数&#xff0c;事实…

64位进程和32位进程通信问题,接收端收不到 SendMessage发送的消息

最近在做一个项目的时候&#xff0c;采用了win32的SendMessage方法来发送数据&#xff0c;本来都没问题&#xff0c;后来增加了一个项目&#xff0c;必须采用的目标平台是x64的&#xff0c;没想到居然没办法通信了。 网上找了很久解决方案&#xff0c;整整尝试了一个周&#xf…