手动编写解析代码的两种清晰、常用的方法是实现一个确定性有限自动机(DFA) 或者一个堆栈自动机(PDA),又或者是采用递归下降解析的方法。
对于像这样的简单任务,使用状态机是一个不错的选择。基于实际工作经验,我有很多次做类似事情的经历,可以提供一些建议:为了让手动编写的状态机代码易于阅读和维护,关键在于定义有意义的状态,这样通过观察就能明白每个状态中应该发生什么。
利用Myhill-Nerode定理来决定你的状态应该是什么,这样每个状态都可以根据它所匹配的内容来命名——易于阅读——并且你会得到尽可能少的状态数。
综合起来,你的代码可能会像下面这样:
// 解析状态。名称即其所匹配的内容(依据Myhill-Nerode)
// 名称为MAYBE_*的状态是有效的终点,且编号为负数
static final int ST_NUMBER = 0;
static final int ST_POSITIVE_NUMBER = 1;
static final int ST_MAYBE_FRACTION = -2;
static final int ST_MAYBE_POSITIVE_OR_FRACTION = -3;
static final int ST_FRACTION_DIGITS = 4;
static final int ST_MAYBE_FRACTION_DIGITS = -5;
public static boolean isValidNumber(final String s) {
int state = ST_NUMBER;
for (int i = 0; i < s.length(); i++) {
final char c = s.charAt(i);
switch (state) {
default:
// 不可能发生的情况
return false;
case ST_NUMBER:
if (c != '-') {
--i; // 再次处理这个字符
}
state = ST_POSITIVE_NUMBER;
break;
// ... 省略了其他case分支以保持简洁 ...
case ST_FRACTION_DIGITS:
case ST_MAYBE_FRACTION_DIGITS:
// 只允许数字
if (c < '0' || c > '9') {
return false;
}
state = ST_MAYBE_FRACTION_DIGITS;
break;
}
}
// 到达字符串末尾。如果我们在一个MAYBE_状态,则为有效
return state < 0;
}
另外,如评论中所述,递归下降同样也是解决此问题的一个合理方法。那样的话,代码看起来会是这样:
public static boolean isValidNumber(final String s) {
return matchNumber(s, 0) == s.length();
}
private static int matchNumber(String s, int pos) {
int subPos = matchChar(s, pos, '-');
if (subPos > pos) {
pos = subPos;
}
pos = matchPositiveInt(s, pos);
if (pos < 0) {
return -1;
}
subPos = matchChar(s, pos, '.');
if (subPos > pos) {
pos = matchDigits(s, subPos);
if (pos < 0) {
return -1;
}
}
return pos;
}
private static int matchPositiveInt(String s, int pos) {
int subPos = matchChar(s, pos, '0');
if (subPos > pos) {
return subPos;
}
return matchDigits(s,pos);
}
private static int matchDigits(String s, int pos) {
int ret = -1;
for(;pos < s.length(); ++pos) {
char c = s.charAt(pos);
if (c < '0' || c > '9') {
break;
}
ret = pos+1;
}
return ret;
}
private static int matchChar(String s, int pos, char c) {
if (pos < s.length() && s.charAt(pos) == c) {
return pos + 1;
}
return -1;
}