必须解析输入字符串。由于它可以包含嵌套的大括号,我们需要一个递归解析器。但首先,我们需要一个数据模型来表示树状结构。
我们可以在这棵树中拥有三种不同类型的项目:文本、表示序列的列表和表示选择的列表。让我们从这个抽象基类派生出三个类:
abstract public class TreeItem
{
public abstract string GetRandomSentence();
}
TextItem 类只是将其文本作为“随机句子”返回:
public class TextItem : TreeItem
{
public TextItem(string text)
{
Text = text;
}
public string Text { get; }
public override string GetRandomSentence()
{
return Text;
}
}
序列连接其项目的文本:
public class SequenceItem : TreeItem
{
public SequenceItem(List<TreeItem> items)
{
Items = items;
}
public List<TreeItem> Items { get; }
public override string GetRandomSentence()
{
var sb = new StringBuilder();
foreach (var item in Items) {
sb.Append(item.GetRandomSentence());
}
return sb.ToString();
}
}
选择项是唯一使用随机性从列表中选择一项的随机项:
public class ChoiceItem : TreeItem
{
private static readonly Random _random = new();
public ChoiceItem(List<TreeItem> items)
{
Items = items;
}
public List<TreeItem> Items { get; }
public override string GetRandomSentence()
{
int index = _random.Next(Items.Count);
return Items[index].GetRandomSentence();
}
}
请注意,序列和选择项都在其项目上递归调用 GetRandomSentence() 以递归地下降树。
这是最简单的部分。现在让我们创建一个解析器。
public class Parser
{
enum Token { Text, LeftBrace, RightBrace, Comma, EndOfString }
int _index;
string _definition;
Token _token;
string _text; // If token is Token.Text;
public TreeItem Parse(string definition)
{
_index = 0;
_definition = definition;
GetToken();
return Choice();
}
private void GetToken()
{
if (_index >= _definition.Length) {
_token = Token.EndOfString;
return;
}
switch (_definition[_index]) {
case '{':
_index++;
_token = Token.LeftBrace;
break;
case '}':
_index++;
_token = Token.RightBrace;
break;
case ',':
_index++;
_token = Token.Comma;
break;
default:
int startIndex = _index;
do {
_index++;
} while (_index < _definition.Length & !"{},".Contains(_definition[_index]));
_text = _definition[startIndex.._index];
_token = Token.Text;
break;
}
}
private TreeItem Choice()
{
var items = new List<TreeItem>();
while (_token != Token.EndOfString && _token != Token.RightBrace) {
items.Add(Sequence());
if (_token == Token.Comma) {
GetToken();
}
}
if (items.Count == 0) {
return new TextItem("");
}
if (items.Count == 1) {
return items[0];
}
return new ChoiceItem(items);
}
private TreeItem Sequence()
{
var items = new List<TreeItem>();
while (true) {
if (_token == Token.Text) {
items.Add(new TextItem(_text));
GetToken();
} else if (_token == Token.LeftBrace) {
GetToken();
items.Add(Choice());
if (_token == Token.RightBrace) {
GetToken();
}
} else {
break;
}
}
if (items.Count == 0) {
return new TextItem("");
}
if (items.Count == 1) {
return items[0];
}
return new SequenceItem(items);
}
}
它由一个词法分析器组成,即一种将输入文本拆分为标记的低级机制。我们有四种标记:文本、“{”、“}”和“,”。我们将这些标记表示为
enum Token { Text, LeftBrace, RightBrace, Comma, EndOfString }
我们还添加了一个EndOfString 标记来告诉解析器已到达输入字符串的末尾。当令牌为Text 时,我们将此文本存储在字段_text 中。词法分析器由GetToken() 方法实现,该方法没有返回值,而是设置_token 字段,以使当前标记在Choice() 和Sequence() 两种解析方法中可用。
一个困难是当我们遇到一个项目时,我们不知道它是单个项目还是序列或选择的一部分。我们假设整个句子定义是一个由序列组成的选择,这使得序列优先于选择(就像数学中的“*”优先于“+”)。
Choice 和 Sequence 都将项目收集到一个临时列表中。如果此列表仅包含一项,则将返回此项而不是选择列表或序列列表。
你可以像这样测试这个解析器:
const string example = "{{Hello,Hi,Hey} {world,earth},{Goodbye,farewell} {planet,rock,globe{.,!}}}";
var parser = new Parser();
var tree = parser.Parse(example);
for (int i = 0; i < 20; i++) {
Console.WriteLine(tree.GetRandomSentence());
}
输出可能如下所示:
再见摇滚
你好地球
再见地球。
嘿世界
再见摇滚
你好地球
嘿地球
告别星球
再见地球。
嘿世界
再见星球
你好世界
你好世界
再见星球
嘿地球
告别地球仪!
再见地球。
再见地球。
再见星球
告别摇滚