你好呀,我是歪歪。
周末的时候,我在网上看到一个关于微信钱包提现时,手续费收取的一个问题。

说真的,就这个问题吧,我个人觉得,放眼整个金融界,乃至于整个弱智吧,甚至于整个东半球,这都是一个相当炸裂的问题啊。
一时间,我居然恍惚了起来:一眼看去,漏洞百出。但是仔细分析之后,居然 TMD 无懈可击?!
哎呀,这个问题,你就不能细琢磨,一琢磨,脑瓜仁就疼。

你知道的,我是一个行动派,所以我肯定得先验证一波微信提现的手续费是否是这么多。
于是我发起提现了,发现确实是有至少一角钱的手续费:

另外,我发现安卓的手机,在这个页面中还无法截图,所以我就只有通过拍照的方式搞到这个图片了,所以看起来有点别扭,你多担待一下小老弟。
那么第一个问题就来了:我提现一角,它手续费一角。请问我最终到手是多少钱呢?

0.1(元)-0.1(元)=0(元),提现的钱全部扣了手续费,所以没有钱到手。
对吗?
逻辑上是合理的,但是如果微信真的敢这样做的话,不就显得很可(傻)爱(逼)吗?
给你举个例子:假设,我找你借了 100 元钱,然后我通过微信还给你的银行卡(假设微信支持这个功能,类似于跨行转账),此时这笔转账对应的手续费是 1 元。如果从转账的金额里面扣除,你收到的钱是 99 元。
你觉得这合理吗?
如果你觉得合理的话,那么请借给我 1w 吧,我应个急,十分钟后就还你 9900 元。
什么,你问我还有 100 元呢?
别问,问就是手续费,你找微信要去。
所以,正常的逻辑是从你的余额里面扣除。比如在我余额还有 141.02 元的时候,我提取了一毛钱,那么我的余额变成了 140.82 元:

这里,我还隐藏了一个逻辑。
比如你只提现一分钱的时候,如果你的微信余额大于 0.1 元,那么也是要收取 0.1 元的手续费的。
这句话,听起来就想要流泪。

既然是从余额中扣除,那么当我余额只有一毛钱的时候,我再次提现一毛钱,这个时候余额不够扣了,会出现什么情况呢?
我也赶紧试了试:
140.82(元)-140.72(元)=0.1(元)
所以,我先给 Max 同学转给了 140.72(元):

此时我的微信钱包只剩下了 0.1 元:

这个时候,我再次提现 0.1 元的时候,发现微信居然告诉我:本次提现免费!!!

由此可得,当微信里面剩下的钱不够扣手续费的时候,本次提现就会免费。
这个免费,圈起来,后面要考。
实验做完了,先把钱要回来再说:

啊!
大意了啊!
这样一来,我这篇文章的成本就很高了啊。我居然一时间被实验冲昏了头脑,主动上交了私房钱?

但是我还有一个实验场景没有做啊?
就是当我微信钱包里面的钱大于 0.1 元钱的时候,我点击“全部提现”会出现什么场景呢?
于是我以做实验的正当理由,成功的要回了一分钱:

这样,我的余额就变成了 0.11 元:

于是,当我点击“全部提现”的时候,虽然我已经预想到是这个场景了,但是我整个人还是沉默了,深深地沉默了。
我提现 0.11 元,手续费 0.1 元,到账 0.01 元。
也就是说,当你全部提现,且提现的金额大于手续费,即 0.1 元的时候,微信的逻辑是从你提取的钱里面扣除手续费。
也就是说我前面举得转账的例子,是真的有可能出现转出去,钱少了的情况。

麻绳专挑细处断,厄运专找苦命人啊!

现在,我已经得到结论了,所以我不能再输入密码了,再输入密码,又得痛失一毛钱!
实验现在已经结束了,结论我们也已经有了。
那么,接下来,我们再看看最开始的,那个让整个弱智吧,都为之“炸裂”的问题:

通过上面的实验,我们得知,这个问题中的这句话“如果我每次都只取 0.1,然后它手续费收 0.1”是没有任何问题的。
后半句:“就等于我一分钱都没有拿到”。
这句话是值得商榷的,因为通过实验证明,我最开始的时候,确实银行卡到账了 0.1 元。
但是,你要注意,我说“但是”了。

比如,我 1 元钱,每次提取 0.1 元,手续费 0.1 元,这样 5 次之后我微信里面的 1 元钱就变成了银行卡 0.5 元和微信收取的手续费 0.5 元。
那么,如果...
我是说如果,我把我银行卡里面的 0.5 元再次充回到微信里面,继续重复上面的动作,事情是不是就开始变得有趣了?

所以,面试编程题就来了,请听题:
已知,在微信钱包提现任意金额,都会收取至少 0.1 元的手续费,但是当余额不足 0.1 元时除外。假设,小明现在有 100 元,他应该怎么操作,才能把这 100 元钱,尽可能的全部变成手续费,白白送给微信?请给我一段 Java 代码,入参是微信钱包里面的余额,日志打印出对应的操作过程。
拿到题,先不慌,分析一波。
首先,100 元,如果我每次只提取 0.1 元,收取 0.1 元手续费,那么当*操我**作 500 次之后,我还有 50 元。500 次,刚好是微信余额,100 元乘以 10,单位转化为角之后,再除以 2。
再把 50 元,存回去分 250 次取出来。250 次,刚好是微信余额,50 元乘以 10,单位转化为角之后,再除以 2。
再把 25 元存回去分 125 次 取出来。125 次,刚好是微信余额,25 元乘以 10,单位转化为角之后,再除以 2。
再把 12.5 块存回去分 62 次取出来,...
再把 6.2 存回去分 31 次取出来,...
循环往复,对吧。
也就说我每操作一次,我的微信余额会少 0.2 元。
结合前面举得例子,不难推理出来,我每一轮的操作次数,等于微信余额乘以 10,单位转化为角之后,再除以 2。
这个程序不难吧,起手就来:
public static void sbBehavior(double amount) {
//应该还有 amount 小于 0 的边界条件,节约篇幅,不写了。
if (amount <= 0.1) {
//微信里的钱不够了扣手续费了,操作结束
System.out.println("麻花藤:你只剩下:" + amount + "元了,谢谢老铁~");
return;
}
//金额扩大十倍,元转角,好计算
double totalJiao = amount * 10;
//每一轮的操作次数,等于微信余额除以 2
int count = (int) (totalJiao / 2);
//每一轮结束之后,共计手续费
double fee = count * 0.1;
//每一轮结束之后,银行卡里剩下的钱
double remainder = count * 0.1;
System.out.println("微信钱包原金额 = " + amount + "元,操作次数=" + count + "次,手续费=" + fee + "元,剩余金额=" + remainder + "元");
//把银行卡里剩下的钱充回微信,开始下一轮
sbBehavior(remainder);
}
好,按照前面的思路,我写出了这个程序,你就先看这个程序有什么问题。我就明着告诉你,这个程序肯定是有问题的,你就去琢磨,到底有哪些问题。

来,我问你:谁教你金额计算用浮点型的?回去等通知吧。
当我们的入参为 100 的时候,上面那个程序跑完之后,你会发现结果是这样的:

所以,牢记在心,只要涉及到金额的计算,一定一定一定要用 BigDecimal。而且我还附送你一条职场保命心经:用到 BigDecimal 时,具体保留多少小数位,具体的四舍五入规则,一定一定一定要让需求提出方白纸黑字的写在需求里面,而不是你自己想当然的认为,保留两位小数,采用四舍五入就行。后面出问题了,你啪的一下,就是把需求拿出来,你就不会很被动了。
回到我们的程序中,所以我们应该把程序修改成这样:
public static void sbBehavior(BigDecimal amount) {
if (amount.compareTo(new BigDecimal(0.1)) <= 0) {
//微信里的钱不够了扣手续费了,操作结束
System.out.println("麻花藤:你只剩下:" + amount + "元了,谢谢老铁~");
return;
}
//金额扩大十倍,元转角,好计算
BigDecimal jiao = amount.multiply(BigDecimal.TEN);
//每一轮的操作次数,等于微信余额除以 2
BigDecimal count = jiao.divide(new BigDecimal(2), 0, RoundingMode.DOWN);
//每一轮结束之后,共计手续费
BigDecimal fee = count.multiply(new BigDecimal(0.1));
//每一轮结束之后,银行卡里剩下的钱
BigDecimal remainder = count.multiply(new BigDecimal(0.1));
System.out.println("微信钱包原金额 = " + amount + "元,操作次数=" + count + "次,手续费=" + fee + "元,剩余金额=" + remainder + "元");
//把银行卡里剩下的钱充回微信,开始下一轮
sbBehavior(remainder);
}
在上面的程序中,我把参与运行的地方全部都改成了 BigDecimal。但是这个程序还是有问题。
来,你继续去琢磨,到底有哪些问题?

来,我问你:谁教你用 BigDecimal 参与计算的时候,用 new BigDecimal(0.1) 这个构造方法?
你用这个方法,idea 都会提醒你:老铁,听哥哥一句劝,还是用 String 类型的构造函数稳妥一点。

就上面这个程序,我给你跑一下,你就发现问题了,同样还是有浮点数的问题:

所以,程序还得改一下,改成用 BigDecimal 的 String 类型的构造函数,其他啥都不动:

好,这个问题算是解决了。
你继续说,这个程序还有啥问题?

如果你没看出来的话,那么我带你看看输出结果:

在这一次输出的时候,手续费 6.2 元,剩余金额 6.2 元,加起来才 12.4 元。但是我“微信钱包原金额”是 12.5 元啊?
还有一分钱去哪里了呢?
所以我在一开始分析题的时候就给你下了一个套:
100 元,操作 500 次之后,还有 50 元。50 元,操作 250 次之后,还有 25 元。25 元,操作 150 次之后,还有 12.5 元...
如果你没有带着自己的思考看文章的话,那么你可能就默认为操作一次之后,手续费和银行卡的余额都会增加 0.1 元。
也就是手续费和银行卡的金额,和操作次数相关,所以写出了这样的代码:

手续费,确实是每操作一次之后扣除 0.1 元,确实是和操作次数正相关。但是剩余的钱,应该是用当前这一轮剩余的总金额减去当前这一轮的总手续费。
也就是要把这一行代码修改为这样:

拿着这个程序去跑的时候,你会发现输出正常了,每一轮的金额加起来都能相等了:

这下没有任何毛病了。
那么,注意,我现在要开始变形了。我要把题目变成:
请给我一段 Java 代码,入参是微信钱包里面的余额,出参是一共需要操作多少次。
在题目中,加了总操作次数的出参,我已经知道了每一轮操作的次数,算总次数这还不是手到擒来的事情?
分分钟拿出代码:

跑出结果:

我们可以看到是 999 次。是的,不要质疑这个结果,当你有 100 元钱的时候,只需要操作 999 次,你就把自己的 99.9 元都给到微信了。
诶,朋友,你注意看,当我把金额变成 50 元的时候,总次数就是 499 了:

当我把金额变成 9.9 元的时候,总次数就变成了 98 次:

所以,请注意,我要“所以”了。
所以,如果我只要求操作的总次数,不要求输出过程,那么代码应该是怎么样的?
是不是把金额扩大十倍,变成角票,然后减去自己留下的一角钱,就是操作的总次数:
public static int sbBehavior(BigDecimal amount) {
return amount.multiply(BigDecimal.TEN).subtract(BigDecimal.ONE).intValue();
}
这样不就完事了吗?
你忘记前面的所有内容,仔细的想想,是不是确实是这个道理?

假设你有 100 元,无论你怎么操作,微信每次只会收 0.1 元的手续费,而你最多只会剩下 0.1 元。
那么你肯定得至少操作 999 次啊,这个小弯儿能转过来吧?

好,转过来了,是吧?
我再问你一个问题,假设我只有 0.19 元,我要把钱给微信,最多操作一次,然后我给它 0.1 元对吧?
但是,你用上面我给你的代码跑出来只会,输出是 0:

是的,这个代码还是有问题的。
我就明确的告诉你,这个代码只适用于金额在 100.9 元到 0.19 元之间的数字。
至于为什么,自己去琢磨。但是我不建议你去琢磨,因为这个玩意整个从最开始的地方就走偏了。
现在,请你忘记前面所有的代码,因为前面的代码,全都是错的,我全程都在误导你,让你顺着我的思路走。

其实你回想一下,最开始的时候,我为什么要假设你微信里面只有 100 元钱?
因为 100 元钱对应的手续费,不论你是提取一角钱,还是提取 100 元,100*0.001=0.1元,都刚好是 0.1 元。
然后我就开始告诉你,每次提现到银行卡 0.1 元,手续费 0.1 元,巴拉巴拉巴拉~
但是,你有没有想过,或者是看到哪个部分的时候,才恍然大悟:如果我有 1000 元呢?
如果我有 1000 元,那么我第一次全部提现的话,手续费就是 1 元啊,而不是 0.1 元啊?
所以,你现在回过头去看这行代码,是不是特别的搞笑:

怎么会去先计算次数,再根据次数反算其金额呢?
为了尽快的把钱都给到微信,肯定是每次尽量给到更多的手续费。已知手续费率是固定的,那么提现的金额越高,手续费越高对吧?
所以,正确的操作应该是每次把微信钱包里面的钱全部都取出来,也就是基于微信钱包里面的钱,去计算手续费,计算剩余的钱。
转变了核心思路之后,代码就变成了这样:

我也给你放一个粘过去就能用的代码:
public static int sbBehavior(BigDecimal amount, int totalTimes) {
if (amount.compareTo(new BigDecimal("0.1")) <= 0) {
//微信里的钱不够了扣手续费了,操作结束
System.out.println("麻花藤:你只剩下:" + amount + "元了,谢谢老铁~");
return totalTimes;
}
//基于微信钱包里面的钱,去计算手续费
BigDecimal fee = amount.multiply(new BigDecimal("0.001")).setScale(2, BigDecimal.ROUND_UP);
//手续费不足 0.1 元,则补齐为 0.1 元
if (fee.compareTo(new BigDecimal("0.1")) <= 0) {
fee = new BigDecimal("0.1");
}
//提现到银行卡的钱
BigDecimal remainder = amount.subtract(fee);
totalTimes++;
System.out.println("原现金 = " + amount + "元,操作=" + totalTimes + "次后,手续费=" + fee + "元,还剩下=" + remainder + "元");
//把银行卡里剩下的钱充回微信,开始下一轮
return sbBehavior(remainder, totalTimes);
}
这样,当我们有 1000 元的时候,每次做“全部提现”的动作,只需要操作 3257 次:


如果我们还是用之前一毛钱一毛钱的提法,得搞 9999 次。
效率提升 300%+。
舒服了!
而且这个是通用的逻辑,你就算给它 100 元,它也能给你算出是 999 次:

给它 0.19 元,它能给你算出是 1 次:

没有任何毛病,但是,不知道你看到这里,是否产生了一个疑问:为什么我们要把钱尽可能的给微信呢?

那我给你换个角度:我们应该怎么操作,才应该避免给微信手续费呢?
这样一想,是不是思路就打开了?

最后,如果这篇文章有那么一个瞬间,让你笑了一下的话,那么,求个免费的“赞”,不过分吧?