65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C# 中将数字转换为文本

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (15投票s)

2017 年 1 月 9 日

CPOL

4分钟阅读

viewsIcon

55612

downloadIcon

1989

一个简单的程序,用于将数字翻译成它们的文本表示形式。(将 1013 转换为“one thousand, thirteen”)。

引言

在一个下雪的午后,我决定给自己一个小小的编程挑战。要“教会电脑”如何将数字(整数)翻译成文字,有多难呢?我的最终目标是创建一个简单的 WPF 控件,带有一个滚动条,该滚动条绑定到一个标签,可以显示所有正整数,还有一个第二个标签,可以将这些数字转换为它们的文本表示形式。我对结果很满意,认为这可以成为我第一次提交给 CodeProject 的好文章。

背景

我想利用这次练习来重新巩固我的 TDD、WPF 和算法设计技能。我从头开始构建算法,以 TDD 作为我的指导。转换器完成后,我使用 WPF 和 xaml 添加了图形前端。我将所有三个模块分离到独立的项目/程序集中,以帮助保持关注点分离

使用代码

当你构建和运行代码时,你将看到这个简单的界面

screenshot

如上所述,代码被分成了几个独立的模块

1. GUI(WPF/XAML)

这个项目的 xaml 非常基础。

<Window x:Class="GUI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:c="clr-namespace:GUI.Converters"
    Title="Number Converter" Height="169" Width="657" WindowStartupLocation="CenterScreen">
    
    <Window.Resources>
        <c:MyDoubleConverter x:Key="myDoubleConverter"/>
        <c:MyIntToStringConverter x:Key="myIntToStringConverter"/>
    </Window.Resources>

    <StackPanel>
        <Label Content="Slide the ScrollBar back and forth and watch the numbers change:"/>
        <ScrollBar Orientation="Horizontal" Height="40" Name="mySB" Maximum="2147483647" LargeChange="100" SmallChange="1"/>
        <Label x:Name="labSbNumeric" Content="{Binding Path=Value,
            Converter={StaticResource myDoubleConverter}, ElementName=mySB}"/>
        <Label x:Name="labSbString" Content="{Binding Path=Content, 
            Converter={StaticResource myIntToStringConverter}, ElementName=labSbNumeric}"/>
    </StackPanel>
</Window>

StackPanel 由几个小的控件组成。首先,我添加了一个简单的水平滚动条作为数字选择控件。然后,使用基本的数据绑定,我将第一个标签绑定到滚动条的值,将第二个标签绑定到第一个标签的内容。现在,由于滚动条的默认值类型是 double,我需要使用 wpf 风格的转换器将其转换为整数。为了做到这一点,我添加了一个自定义窗口资源对象,指向在单独的文件中定义的转换器(在 GUI.Converters 命名空间中)。我使用了相同的约定将整数转换为文本。

2. 转换器

遵循 MVVM 的约定,避免使用 code-behind 文件,我将转换器代码放在了自己的文件中。如果这是一个更大规模的项目,我会将它们放在 ViewModel 模块中,但对于这样一个最小化的应用程序,我就没 bother 了。

整数转换器非常简单,不过是基本的样板代码

public class MyDoubleConverter : IValueConverter
    {
        // Converts double to int
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            double v = (double)value;
            return (int)v;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value;
        }
    }

同样,int 到 text 的转换器也相当直接,因为所有的计算都在一个单独的库中完成

public class MyIntToStringConverter : IValueConverter
    {
        // Converts int to textual representation
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            int v = (int)value;
            return Converter.ConvertNumberToString(v);
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value;
        }
    }

3. 主转换库

一个单独的库包含了转换函数的所有核心逻辑。该库包含一个静态类(名为 Converter),该类又包含一个公共静态方法 ConvertNumberToString(int),以及几个私有的辅助方法。

辅助方法包括三个非常简单的硬编码映射(它们本质上是后续递归的“基本情况”)。

    private static string ConvertDigitToString(int i)
        {
            switch (i)
            {
                case 0: return "";
                case 1: return "one";
                case 2: return "two";
                case 3: return "three";
                case 4: return "four";
                case 5: return "five";
                case 6: return "six";
                case 7: return "seven";
                case 8: return "eight";
                case 9: return "nine";
                default:
                    throw new IndexOutOfRangeException(String.Format("{0} not a digit",i));
            }
        }

        //assumes a number between 10 & 19
        private static string ConvertTeensToString(int n)
        {
            switch (n)
            {
                case 10: return "ten";
                case 11: return "eleven";
                case 12: return "twelve";
                case 13: return "thirteen";
                case 14: return "fourteen";
                case 15: return "fiveteen";
                case 16: return "sixteen";
                case 17: return "seventeen";
                case 18: return "eighteen";
                case 19: return "nineteen";
                default:
                    throw new IndexOutOfRangeException(String.Format("{0} not a teen", n));
            }
        }

        //assumes a number between 20 and 99
        private static string ConvertHighTensToString(int n)
        {
            int tensDigit = (int)( Math.Floor((double)n / 10.0));

            string tensStr;
            switch (tensDigit)
            {
                case 2: tensStr = "twenty"; break;
                case 3: tensStr = "thirty"; break;
                case 4: tensStr = "forty"; break;
                case 5: tensStr = "fifty"; break;
                case 6: tensStr = "sixty"; break;
                case 7: tensStr = "seventy"; break;
                case 8: tensStr = "eighty"; break;
                case 9: tensStr = "ninety"; break;
                default:
                    throw new IndexOutOfRangeException(String.Format("{0} not in range 20-99", n));
            }
            if (n % 10 == 0) return tensStr;
            string onesStr = ConvertDigitToString(n - tensDigit * 10);
            return tensStr + "-" + onesStr;
        }

较大的数字会稍微有趣一些。为此,我创建了一个方法,它接受三个参数:

        /// <summary>
        /// This is the primary conversion method which can convert any integer bigger than 99
        /// </summary>
        /// <param name="n">The numeric value of the integer to be translated ("textified")</param>
        /// <param name="baseNum">Represents the order of magnitude of the number (e.g., 100 or 1000 or 1e6, etc)</param>
        /// <param name="baseNumStr">The string representation of the base number (e.g. "hundred", "thousand", or "million", etc)</param>
        /// <returns>Textual representation of any integer</returns>
        private static string ConvertBigNumberToString(int n, int baseNum, string baseNumStr)
        {
            // special case: use commas to separate portions of the number, unless we are in the hundreds
            string separator = (baseNumStr != "hundred") ? ", " : " ";

            // Strategy: translate the first portion of the number, then recursively translate the remaining sections.
            // Step 1: strip off first portion, and convert it to string:
            int bigPart = (int)(Math.Floor((double)n / baseNum));
            string bigPartStr = ConvertNumberToString(bigPart) + " " + baseNumStr;
            // Step 2: check to see whether we're done:
            if (n % baseNum == 0) return bigPartStr;
            // Step 3: concatenate 1st part of string with recursively generated remainder:
            int restOfNumber = n - bigPart * baseNum;
            return bigPartStr + separator + ConvertNumberToString(restOfNumber);
        }

第一个参数是数字本身。第二个参数(baseNum)指定数字的“大小”(是 100 的倍数、1000 的倍数,还是 100000 的倍数等)。最后一个参数(baseNumStr)指定该数量级的文本版本(例如,“hundred”、“thousand”等)。这显然是整个程序中最棘手的部分。内联注释解释了每一步的逻辑。也许理解它的最好方法是看看数字如何通过每一步,以下面的例子为例

要转换的数字:2056(baseNum= 1000;baseNumStr="thousand")

第一步之后

  • bigPart =2 056/1000 = 2
  • bigPartStr = "2 thousand"

在第三步

  • restOfNumber = 2056 - (2*1000) = 56 <-- 这是需要递归转换为字符串的“余数”。

最后,我们来看主要的公共转换方法,它实际上只是一个简单的映射函数,根据需要调用其他实用/辅助方法。

      //converts any number between 0 & INT_MAX (2,147,483,647)
        public static string ConvertNumberToString(int n)
        {
            if (n < 0)
                throw new NotSupportedException("negative numbers not supported");
            if (n == 0)
                return "zero";
            if (n < 10)
                return ConvertDigitToString(n);
            if (n < 20)
                return ConvertTeensToString(n);
            if (n < 100)
                return ConvertHighTensToString(n);
            if (n < 1000)
                return ConvertBigNumberToString(n, (int)1e2, "hundred");
            if (n < 1e6)
                return ConvertBigNumberToString(n, (int)1e3, "thousand");
            if (n < 1e9)
                return ConvertBigNumberToString(n, (int)1e6, "million");
            //if (n < 1e12)
            return ConvertBigNumberToString(n, (int)1e9, "billion");
        }

就是这样。很简单,真的。但这却是一个很棒的小练习,也是一个在雪天下午进行的有趣项目。

关注点

TDD 策略被证明非常有帮助。我首先创建了一个测试来验证单个数字的转换,然后继续创建越来越具有挑战性的转换测试(十几、几十、几百、几千等)。在每个阶段,都很容易看出如何利用前一阶段的代码来构建。

主转换函数(ConvertBigNumberToString())的最终版本相当简洁。它最初不是这样的。事实上,我一开始有多个方法(一个用于“hundreds”,一个用于“thousands”,一个用于“millions”,等等)。你仍然可以看到一些证据,在(现在被注释掉的)单元测试中有一些。最终我看到了所有方法共有的模式,这使我能够将算法压缩成一个递归方法。我喜欢它的简洁,但不得不承认由此产生的代码有点难以理解。

结论

我一直想在 CodeProject 上发一篇文章,所以我很高兴终于做到了。我欢迎任何有经验的读者给我关于如何改进文章(或相关代码)的建议。

附件

  • NumberConverter.exe.zip -- 下载、解压缩并运行
  • NumberConverterToText.zip -- 源代码和项目文件(包括单元测试)
© . All rights reserved.