Klesh.Cn

concentrating on knowing more...

敏捷开发解决实数转中文大写金额问题

  实数转中文大写的问题,虽然不能算是太难,但却也不是那种能一气呵成,一蹴而就的简单问题,一步到位的想法很容易就会陷入泥潭;正确的做法应该是对转换的规律抽丝剥茧,由浅入深一步一步完成转换步骤,如此便能水到渠成……敏捷开发的思想很适用于解决此类问题,借此机会正好和大家分享一些敏捷开发的经验。

  开始之前,先看一下大写位进换数情况先,这里以目前财务体系的中法换算为准:

个=10的0次方
十=10的1次方
百=10的2次方
千=10的3次方

万=10的4次方
亿=10的8次方
兆=10的12次方
京=10的16次方
垓=10的20次方
杼=10的24次方
穰=10的28次方
沟=10的32次方
涧=10的36次方
正=10的40次方
载=10的44次方
极=10的48次方
恒河砂=10的52次方
阿僧祇=10的56次方
不可思议=10的60次方

  在中法换算中,“个十百千”可认为是基本单位,而从开始会复用这些基本单位,用完这4个基本单位之后就会有新的单位出现。然后再利用这4个基本单位,如此循环往复。因此,首要解决的就是“个十百千”这个级别上的转换问题,一则是因为它最简单,二则是它的转换逻辑还可以为以后更高层次的转换所复用。

  先考虑最简单的情形,即不带零和小数的情况:

        [Test]
        public void SimpleConvert()
        {
            CurrencyConvert convert = new CurrencyConvert(1234M);
            Assert.AreEqual("壹仟贰佰叁拾肆元整", convert.ToString());
        }

  没有小数,也先不考虑零的情况。在我们编写类来通过测试之前,先分析一下大写转换规律:

〈────────────
1 2 3 4
 

  上表中,第三行,白底部分我们称为数字,灰底部分称为位字,红色的箭头代表我们分析金额时的顺序,是从右至左,第一个数字的位字为,此位字不作输出;第二个数字的位字为;第三个为;第四个为。那么,只要从右向左分别得出数字及其位字,依次插入到列表的头,再串联起来就可以得到其大写形式了。

    public class CurrencyConvert
    {
        private static string[] symbols1 = new string[] { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" };
        private static string[] symbols2 = new string[] { "", "拾", "佰", "仟" };
        private decimal _source;
 
        public CurrencyConvert(decimal source)
        {
            _source = source;
        }
 
        public override string ToString()
        {
            string sourceString = _source.ToString("F");
            List<string> list = new List<string>();
            for (int i = sourceString.Length - 1, j = 0; i >= 0; i--, j++)
            {
                list.Insert(0, symbols2[j]);
                list.Insert(0, symbols1[int.Parse(sourceString[i].ToString())]);
            }
            return string.Join("", list.ToArray()) + "元整";
        }
    }
 

  现在考虑带零的情况,零的转换有以下几种情况:

  1. 零后面不打印位字(如1024为“壹仟零贰拾肆元整”)
  2. 当有多个零连续出现的时候只打印一个零(如1004为“壹仟零肆元整”)
  3. 连续的零一直延伸到个位的时候不打印零(如1200为“壹仟贰佰元整”)

  针对这带零的情况编写单位测试:

        [Test]
        public void SimpleWithZeroConvert()
        {
            CurrencyConvert convert1 = new CurrencyConvert(1024M);
            Assert.AreEqual("壹仟零贰拾肆元整", convert1.ToString());
            CurrencyConvert convert2 = new CurrencyConvert(1004M);
            Assert.AreEqual("壹仟零肆元整", convert2.ToString());
            CurrencyConvert convert3 = new CurrencyConvert(1200M);
            Assert.AreEqual("壹仟贰佰元整", convert3.ToString());
        }

  情况一容易解决,只要稍加判断当前数字是否为零即可;第二个也不难,只要判断最后一个插入的是不是零即可,但是在进行这个判断之前要先判断列表是否为空,否则取值时会引发异常;第三个情况,看起来相对复杂一点,但其实在第二点判断列表是否为空的时候这个问题也同时被解决了。

    public class CurrencyConvert
    {
        private static string[] symbols1 = new string[] { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" };
        private static string[] symbols2 = new string[] { "", "拾", "佰", "仟" };
        private decimal _source;
 
        public CurrencyConvert(decimal source)
        {
            _source = source;
        }
 
        public override string ToString()
        {
            string sourceString = _source.ToString("F");
            List<string> list = new List<string>();
            for (int i = sourceString.Length - 1, j = 0; i >= 0; i--, j++)
            {
                int number = int.Parse(sourceString[i].ToString());
                if (number == 0)
                {
                    if (list.Count > 0 && list[0] != symbols1[0])
                        list.Insert(0, symbols1[number]);
                }
                else
                {
                    list.Insert(0, symbols2[j]);
                    list.Insert(0, symbols1[number]);
                }
            }
            return string.Join("", list.ToArray()) + "元整";
        }
    }

  好,接下来,在向更高位迈进之前,先来解决一下小数的问题。相对来讲,小数的问题比较容易处理,我可不想在最困难的部分解决之后还要来料理这些细节。小数部分,大概需要考虑以下几种情形:

  1. 如果被转换数小于1,那么不打印“整数部分”的信息,直接打印小数部分且前面不打印“零”
  2. 如果被转换数大于1,则先打印“整数部分”,再打印“小数部分”,中间有多个零时只打印一个零
  3. 如果不存在小数部分,则返回“整”(这个在前面整数部分的测试用例已被覆盖)
        [Test]
        public void DecimalConvert()
        {
            CurrencyConvert convert1 = new CurrencyConvert(0.1234567M);
            Assert.AreEqual("壹角贰分叁厘肆毫伍丝陆忽", convert1.ToString());
            CurrencyConvert convert2 = new CurrencyConvert(0.001M);
            Assert.AreEqual("壹厘", convert2.ToString());
            CurrencyConvert convert3 = new CurrencyConvert(0.01M);
            Assert.AreEqual("壹分", convert3.ToString());
            CurrencyConvert convert4 = new CurrencyConvert(1.1M);
            Assert.AreEqual("壹元壹角", convert4.ToString());
            CurrencyConvert convert5 = new CurrencyConvert(1.001M);
            Assert.AreEqual("壹元零壹厘",convert5.ToString());
        }

  如上所示,小数部分,我们假定精确到“忽”,余下部分就忽略掉。如下图所示,小数部分的拼装顺序和整数部分的拼装顺序略有不同。这时,应该对程序进行重构,把总的拼装逻辑分为整数部分小数部分

  ─────〉
1 . 0 4
 
  重构后的代码:
    public class CurrencyConvert
    {
        private static string[] symbols0 = new string[] { "角", "分", "厘", "毫", "丝", "忽" };
        private static string[] symbols1 = new string[] { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" };
        private static string[] symbols2 = new string[] { "", "拾", "佰", "仟" };
        private decimal _source;
 
        public CurrencyConvert(decimal source)
        {
            _source = source;
        }
 
        public override string ToString()
        {
            string[] ary = _source.ToString("F").Split('.');
            return ConvertIntegerPart(ary[0]) + ConvertDecimalPart(ary.Length == 2 ? ary[1] : string.Empty);
        }
 
        private string ConvertIntegerPart(string integerPart)
        {
            if (string.IsNullOrEmpty(integerPart) || decimal.Parse(integerPart) == 0M) 
                return string.Empty;
            List<string> list = new List<string>();
            for (int i = integerPart.Length - 1, j = 0; i >= 0; i--, j++)
            {
                int number = int.Parse(integerPart[i].ToString());
                if (number == 0)
                {
                    if (list.Count > 0 && list[0] != symbols1[0])
                        list.Insert(0, symbols1[number]);
                }
                else
                {
                    list.Insert(0, symbols2[j]);
                    list.Insert(0, symbols1[number]);
                }
            }
            if (list.Count > 0) list.Add("元");
            return string.Join(string.Empty, list.ToArray());
        }
 
        private string ConvertDecimalPart(string decimalPart)
        {
            if (string.IsNullOrEmpty(decimalPart) || decimal.Parse(decimalPart) == 0M)
                return "整";
            List<string> list = new List<string>();
            for (int i = 0; i < Math.Min(decimalPart.Length, symbols0.Length); i++)
            {
                int number = int.Parse(decimalPart[i].ToString());
                if (number == 0)
                {
                    if ((list.Count == 0 && _source > 1M) || (list.Count > 0 && list[list.Count - 1] != symbols1[0]))
                        list.Add(symbols1[number]);
                }
                else
                {
                    list.Add(symbols1[number]);
                    list.Add(symbols0[i]);
                }
            }
            return string.Join(string.Empty, list.ToArray());
        }
    }

  完成了这个部份之后,我突然想到,如果传进去的是 0 ,则程序会正常输出“零元”吗?,所以我写多了一个测试用例:

        [Test]
        public void ZeroConvert()
        {
            CurrencyConvert convert = new CurrencyConvert(0M);
            Assert.AreEqual("零元", convert.ToString());
        }

  这是一个十分特殊的的case,这时候左右部分都为空,而且原来的拼装法则也用上不了,所以我把它放在了总拼装函数ToString之前作一个特殊化判断。

        public override string ToString()
        {
            if (_source == decimal.Zero) return "零元";
            string[] ary = _source.ToString("F").Split('.');
            return ConvertIntegerPart(ary[0]) + ConvertDecimalPart(ary.Length == 2 ? ary[1] : string.Empty);
        }

  好了,相对简单的部份解决了。现在来研究“万元”以上的情况:

〈──────────────────────────────────────────────
1 2 3 4 5 6 7 8 9 0 1 2
亿    

  再往左的情况就是红色部份会被下一个单位替换,这种简单重复而已。最右边空的其实就是个位,由此我们可以总结出拼装的规律:

  1. 数字每4个一组,从右至左使用“个万亿兆京……”为单位
    需要再重构一次,把原来的整数转换逻辑提取为组的转换ConvertGroup
  2. 每组数字从右至左使用“个十百千”为单位串联
    原来的整数转换逻辑
  3. 当某一组和其右边所有组全为零时,这些组将不被打印
    ConvertGroup的结果稍加判断即可实现

  依旧先从简单开始,编写第一点的测试用列:

        [Test]
        public void TenThousandAboveConvert()
        {
            CurrencyConvert convert1 = new CurrencyConvert(123456789012M);
            Assert.AreEqual("壹仟贰佰叁拾肆亿伍仟陆佰柒拾捌万玖仟零壹拾贰元整", convert1.ToString());
        }

  接下来是实现:

        private string ConvertIntegerPart(string integerPart)
        {
            if (string.IsNullOrEmpty(integerPart) || decimal.Parse(integerPart) == 0M) 
                return string.Empty;
            List<string> list = new List<string>();
            // 开始新的转换逻辑
            const int groupMaxLength = 4;
            int residue = integerPart.Length % groupMaxLength;
            int groupsCount = integerPart.Length / groupMaxLength + (residue > 0 ? 1 : 0);
            int lastGroupCount = residue == 0 ? groupMaxLength : residue;
            for (int i = groupsCount - 1, j = 0; i >= 0 && j < symbols3.Length ; i--, j++) 
            {
                int groupStart = i == 0 ? 0 : lastGroupCount + (i - 1) * groupMaxLength;
                int groupLength = i == 0 ? lastGroupCount : groupMaxLength;
                list.Insert(0, symbols3[j]);
                list.Insert(0, ConvertGroup(integerPart.Substring(groupStart, groupLength)));
            }
            // 完成新的转换逻辑
            if (list.Count > 0) list.Add("元");
            return string.Join(string.Empty, list.ToArray());
        }
        // 原先的简单转换逻辑被提取出来
        private string ConvertGroup(string group)
        {
            List<string> list = new List<string>();
            for (int i = group.Length - 1, j = 0; i >= 0; i--, j++)
            {
                int number = int.Parse(group[i].ToString());
                if (number == 0)
                {
                    if (list.Count > 0 && list[0] != symbols1[0])
                        list.Insert(0, symbols1[number]);
                }
                else
                {
                    list.Insert(0, symbols2[j]);
                    list.Insert(0, symbols1[number]);
                }
            }
            return string.Join(string.Empty, list.ToArray());
        }
 

  组的分割还挺不容易,调试了许久,可惜看起来还是不太优雅,暂时就先这样吧,起码它已经能正确运行。好,现在加上零的情况:

        [Test]
        public void TenThousandAboveConvert()
        {
            CurrencyConvert convert1 = new CurrencyConvert(123456789012M);
            Assert.AreEqual("壹仟贰佰叁拾肆亿伍仟陆佰柒拾捌万玖仟零壹拾贰元整", convert1.ToString());
            CurrencyConvert convert2 = new CurrencyConvert(12300M);
            Assert.AreEqual("壹万贰仟叁佰元整", convert2.ToString());
        }

  支持这个真是相当简单,只要对组的拼装结果进行判断就行了

            for (int i = groupsCount - 1, j = 0; i >= 0 && j < symbols3.Length ; i--, j++) 
            {
                int groupStart = i == 0 ? 0 : lastGroupCount + (i - 1) * groupMaxLength;
                int groupLength = i == 0 ? lastGroupCount : groupMaxLength;
                string groupString = ConvertGroup(integerPart.Substring(groupStart, groupLength));
                if (!string.IsNullOrEmpty(groupString))
                {
                    list.Insert(0, symbols3[j]);
                    list.Insert(0, groupString);
                }
            }

  最后,综合测试:

        [Test]
        public void FinnalTest()
        {
            CurrencyConvert convert = new CurrencyConvert(120004000001.001M);
            Assert.AreEqual("壹仟贰佰亿零肆佰万零壹元零壹厘", convert.ToString());
        }

  测试用例是我们假想出来程序可能遇到的情况,通常这些用例都是有代表性的,但往往不能覆盖每种情形,因此,完成单元测试后,我做了一个简单的winform来做烟雾测试,确定程序能正常运行。整个解决方案已经打包:

  下载TestDrivenDemo

  本来这篇文章本应在昨天完成,但是Copy Source As HTML插件在复制含有中文的代码时总是会有多余的字符出现,所以只好中断去debug CSAH,稍后放出修改好的dll。

  ps:这种题目作笔试题实在是十万分不适合,如果是我一定立马拍屁股走人。

posted on 2007-11-13 15:55  Klesh Wong  阅读(2887)  评论(24编辑  收藏  举报

导航