在 C# 中将数字转换为文本
一个简单的程序,用于将数字翻译成它们的文本表示形式。(将 1013 转换为“one thousand, thirteen”)。
引言
在一个下雪的午后,我决定给自己一个小小的编程挑战。要“教会电脑”如何将数字(整数)翻译成文字,有多难呢?我的最终目标是创建一个简单的 WPF 控件,带有一个滚动条,该滚动条绑定到一个标签,可以显示所有正整数,还有一个第二个标签,可以将这些数字转换为它们的文本表示形式。我对结果很满意,认为这可以成为我第一次提交给 CodeProject 的好文章。
背景
我想利用这次练习来重新巩固我的 TDD、WPF 和算法设计技能。我从头开始构建算法,以 TDD 作为我的指导。转换器完成后,我使用 WPF 和 xaml 添加了图形前端。我将所有三个模块分离到独立的项目/程序集中,以帮助保持关注点分离。
使用代码
当你构建和运行代码时,你将看到这个简单的界面
如上所述,代码被分成了几个独立的模块
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 = 2bigPartStr
= "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 -- 源代码和项目文件(包括单元测试)