将文本数字转换为数值
拼出来!不!等等!
引言
最近,有人在 Lounge 抱怨说,以下文本难以排序
- First
- 第二种
- 第三部分
- ...
- Eleventh
他的想法是,他想根据数字的等价值对上面的列表进行排序。嗯,一旦你将文本转换为它们的实际数值,这实际上并非那么困难,即使转换本身也并不真正困难。这是我的解决方案,它可能是解决问题的方法之一。如果你有其他方法,请随时发布文章。
假设
此代码使用了递归和扩展方法。假定您已经熟悉这些语言特性的概念。
背景
与任何其他编程问题一样,您通常从模糊的需求列表、模糊的用户故事以及(通常)非常具体的结果要求开始 - 这些特质通常与家庭作业有关,但更多时候也反映了您在工作中遇到的现实情况。这个问题遵循这种模式。任何一个有点本事的程序员(如果让他自己决定)都不会提供一个仅仅满足了已陈述要求的解决方案。在这种情况下,示例数据只到“eleventh”,但这并不重要。你只需假定传入的数据不会仅限于提供的样本,而且,你最好尽可能(并且合理地)为更多的意外情况编写代码。
代码
为了实现这个解决方案,我创建了三个类。第一个类 - NumberDictionary - 定义了一个术语及其关联的十进制值字典。
public static class NumberDictionary
{
public static Dictionary<string, decimal=""> Numbers = new Dictionary<string, decimal="">()
{
{"ZERO", 0}, {"TEN", 10}, {"HUNDRED", 100},
{"FIRST", 1}, {"ELEVEN", 11}, {"THOUSAND", 1000},
{"ONE", 1}, {"TWELF", 12}, {"MILLION", 1000000},
{"SECOND", 2}, {"TWELVE", 12}, {"BILLION", 1000000000}, //*
{"TWO", 2}, {"THIRTEEN", 13}, {"MILLIARD", 1000000000},
{"THIRD", 3}, {"FOURTEEN", 14}, {"TRILLION", 1000000000000}, //*
{"THREE", 3}, {"FIFTEEN", 15}, {"QUADRILLION", 1000000000000000},
{"FOUR", 4}, {"SIXTEEN", 16}, {"BILLIARD", 1000000000000000},
{"FIF", 5}, {"SEVENTEEN", 17}, {"QUINTILLION", 1000000000000000000},
{"FIVE", 5}, {"EIGHTEEN", 18}, // I had to stop here because even a
{"SIX", 6}, {"NINETEEN", 19}, // decimal can't hold a sextillion or
{"SEVEN", 7}, {"TWENTY", 20}, // septillion
{"EIGH", 8}, {"THIRTY", 30},
{"NIN", 9}, {"FORTY", 40},
{"NINE", 9}, {"FIFTY", 50},
{"SIXTY", 60},
{"SEVENTY", 70},
{"EIGHTY", 80},
{"NINETY", 90},
};
// * These values are adjusted if the region is a long-scale region
}
</string,></string,>
美国人可能会注意到那些奇怪的词——“BILLIARD”和“MILLIARD”。这些术语在长尺度国家中使用,其中十亿和万亿的解释完全不同。如果你想了解更多关于这些和其他差异的信息,请访问这个 Wiki 页面。
文本处理
此功能包含在一个静态的 string 扩展类中。
对我来说,将面向外的接口设为一个 string 扩展方法是很有意义的。这是一个简单的方法,可以规范化文本并将每个文本组件转换为数值。你可能已经注意到我使用了 decimal 类型。我之所以这样做,是因为它的最大值非常大,远远超过当前代码设定的最大可能值 999 万亿。
这段代码并没有什么特别之处。我只是替换了一些东西,删掉了一些东西,然后添加了一些东西(你不喜欢听程序员轻描淡写代码的样子吗?)以便文本尽可能地规范化。一旦完成,我通过尝试在 Numbers Dictionary 中查找每个组件“单词”来将其转换为其等效的数值。如果我找不到任何一个组件单词,就会抛出一个异常。一旦我们通过了规范化/解析部分,我们就可以进行计算得出数值了。
更新,2015 年 2 月 13 日 - 显然,除了美国之外,所有人都错误地解释了十亿和万亿的数值(笑)。这种差异被称为短尺度(美国)或长尺度(英国、等)。为了安抚提到这个问题的各位,我提出了一个解决方案。我将数字字典移到了它自己的类中,并在这里添加了一个方法来根据长尺度地区进行调整。这个新方法在我们做任何事情之前都会被调用。我没有时间和兴趣包含所有长尺度地区,所以我把它留给程序员一个非常小的练习,将他的/她的 3 字母 ISO 地区代码添加到 longScaleRegions 字符串中。我还添加了一些代码来将“AND”替换为空格,以防有人尝试输入“one hundred and five”这样的内容。
public static class ExtendString
{
private static IntList values;
private static void AdjustForLongScale()
{
string longScaleRegions = "GBR,";
if (longScaleRegions.Contains(RegionInfo.CurrentRegion.ThreeLetterISORegionName))
{
numbers["BILLION"] = numbers["MILLION"] * numbers["MILLION"];
numbers["TRILLION"] = numbers["BILLION"] * numbers["MILLION"];
}
}
public static decimal Translate(this string text)
{
ExtendString.AdjustForLongScale();
text = text.ToUpper().Trim();
string trimChars = "TH";
text = text.Replace("TY", "TY ");
text = text.Replace("-"," ").Replace("_"," ").Replace("."," ").Replace(",", " ");
text = text.Replace(" AND", " ");
if (text.EndsWith(trimChars))
{
text = text.TrimEnd(trimChars.ToArray());
}
text = text.Replace(" ", " ");
values = new IntList();
string[] parts = text.Split(' ');
foreach (string numberText in parts)
{
if (numbers.Keys.Contains(numberText))
{
values.Add(numbers[numberText]);
}
else
{
throw new Exception("Not a number (might be spelled wrong)");
}
}
return values.NumericValue;
}
}
数学
我创建了一个派生自 List<decimal> 的类,它包含解析出的数值,并对这些值执行操作。这有效地分离和包含了我认为是离散的功能。它还有助于保持扩展方法的整洁。给定文本“four hundred twenty nine thousand six hundred six”,在解析后列表的内容开始如下:
- [0] 4
- [1] 100
- [2] 20
- [3] 9
- [4] 1000
- [5] 6
- [6] 100
- [7] 6
考虑到问题的范围,我们最多应该只需要 15 个左右的列表元素(如果你想支持超过 999 万亿,则需要更多),所以我们不必担心这个列表的大小。
我们做的第一件事就是将列表分解成更小的块。这只是为了更容易管理。每个主要数值分组都有一个“迷你列表”,并根据其数值用途命名。
private List<decimal> trillions = new List<decimal>();
private List<decimal> billions = new List<decimal>();
private List<decimal> millions = new List<decimal>();
private List<decimal> thousands = new List<decimal>();
private List<decimal> hundreds = new List<decimal>();
注意:请注意,迷你列表不包含超过万亿的任何内容,也不包含长尺度特有的术语。如果你愿意,可以随意添加它们。
当文本被解析后,扩展方法会检索 IntList.NumericValue 属性。为了检索预期的数值,会执行几个操作。
public decimal NumericValue
{
get
{
this.BuildMiniLists();
this.DoMiniMaths();
this.AddMiniValues();
decimal value = this.Sum(x=>x);
return value;
}
}
每个迷你列表都会被填充(如果需要)。我们通过查找大数值并将其以及所有后续值复制到相应的迷你列表中来实现这一点。
更新 - 2015 年 2 月 13 日 - 我更改了对 BuildMiniList 的调用,使其引用 NumberDictionary 的值。这很有意义,因为你可能处于一个长尺度地区,其中某些字典数字的值可能会发生变化。
private void BuildMiniLists()
{
this.BuildMiniList(this.trillions, NumberDictionary.Numbers["TRILLION"]);
this.BuildMiniList(this.billions, NumberDictionary.Numbers["BILLION"]);
this.BuildMiniList(this.millions, NumberDictionary.Numbers["MILLION"]);
this.BuildMiniList(this.thousands, NumberDictionary.Numbers["THOUSAND"]);
this.BuildMiniList(this.hundreds, NumberDictionary.Numbers["HUNDRED"]);
}
private void BuildMiniList(List<decimal> list, decimal amount) {
// find the index of the specified amount
int index = this.IndexOf(amount);
// if we have an index if (index >= 0)
{
// copy the values from 0 to the found index into the
// specified mini list
decimal[] values;
values = new decimal[index+1];
this.CopyTo(0, values, 0, index+1);
list.AddRange(values);
// and remove those items from the parent list
for (int i = index; i >= 0; i--)
{
this.RemoveAt(i);
}
}
}
接下来,对每个迷你列表执行计算。DoMath 方法是递归的,它会遍历指定的列表,查找要乘以或加上的内容。使用我们上面的例子,thousands 迷你列表看起来会是这样
- [0] 4
- [1] 100
- [2] 20
- [3] 9
- [4] 1000
下面的 DoMath 方法将通过读取每个值并将其加或乘到结果值中来构建相应的值。处理过程会这样做:
((4 * 100) + 20 + 9) * 1000) = 429000
处理后,迷你列表将如下所示:
- [0] 0
- [1] 0
- [2] 0
- [3] 0
- [4] 429000
private void DoMiniMaths()
{
this.DoMath(this.trillions, 0);
this.DoMath(this.billions, 0);
this.DoMath(this.millions, 0);
this.DoMath(this.thousands, 0);
this.DoMath(this.hundreds, 0);
}
private void DoMath(List<decimal> list, int lastBigIndex)
{
if (list.Count > 0)
{
decimal rollingValue = 0;
decimal[] big = new decimal[]{100,1000,1000000,1000000000,1000000000000};
for (int j = 0; j < list.Count; j++)
{
if (big.Contains(list[j]))
{
rollingValue = Math.Max(1, rollingValue) * list[j];
list[j] = rollingValue;
for (int i = lastBigIndex; i < j; i++)
{
list[i] = 0;
}
lastBigIndex = j;
if (j < list.Count - 1)
{
this.DoMath(list, lastBigIndex);
}
}
else
{
rollingValue += list[j];
}
}
}
}
在对每个迷你列表执行计算后,我们将每个列表中的值相加,并将每个总和加回到父列表中。
private void AddMiniValues()
{
this.Add(this.trillions.Sum(x=>x));
this.Add(this.billions.Sum(x=>x));
this.Add(this.millions.Sum(x=>x));
this.Add(this.thousands.Sum(x=>x));
this.Add(this.hundreds.Sum(x=>x));
}
对迷你列表求和后,父列表应包含以下内容:
- [0] 6
- [1] 100
- [2] 6
- [3] 429000
当对父列表中的值求和时,我们得到结果“429606”。现在我们已经写完了所有这些代码,我将把它留给程序员来完成将得到的数值与文本关联起来进行排序的练习。
Using the Code
用法大致如下(从控制台应用程序):
class Program
{
static void Main(string[] args)
{
Translate("twelfth");
Translate("First");
Translate("One Hundredth");
Translate("five thousand seven Hundred thirtysecond");
Translate("four hundred twenty nine thousand");
Translate("four hundred twenty nine thousand six hundred six");
Translate("fortyfive");
Translate("twenty seven million two hundred thirtyfour thousand one");
Console.ReadKey();
}
static void Translate(string text)
{
decimal value = text.Translate();
Console.WriteLine(string.Format("{0} - {1:#,##0}", text, value));
}
}
关注点
在编写此代码时,没有任何特别奇怪或意外的事情发生,但它很有趣,让我在日常工作之外做了一些脑力锻炼。我是一名程序员。
历史
- 2018 年 2 月 27 日 修复了一些拼写错误和不完整的 HTML 标签。
- 2015 年 2 月 13 日 (B) 增加了对长尺度地区的支持。
- 2015 年 2 月 13 日 (A) 格式化了一个代码块并修复了一些拼写错误。
- 2015 年 2 月 12 日 首次发布。