菜单

szdxdxdx
szdxdxdx
发布于 2024-06-02 / 178 阅读
1
0

提取字符串中的数字

介绍

编写一个 C 函数,扫描字符串并提取出位于其开头的数字

识别数字的标准参考下图

图片来源:ECMA-404 The JSON Data Interchange Standard


函数声明:

/// \brief
///     提取出位于 `str` 开头的数字
/// \details
///     尽可能多地扫描字符,直到不再构成合法的数字 \n
///     支持整数、小数和科学计数法 \n
///     整数部分不支持有多余的正号和前导零
/// \return
///     返回该数字最末字符再往后一位字符的地址 \n
///     若 `str` 开头没有数字则返回 `NULL`
char *extract_number(const char *str);

用法示例

#include <stdio.h>

int main(void) {
    system("chcp 65001");
    setbuf(stdout, NULL);

    char str[64];

    while (1) {
        printf("\n请输入字符串:");
        rewind(stdin);
        scanf("%s", str);

        char *num_end = extract_number(str); /* 提取数字 */
        if (NULL == num_end)
            printf("无法从字符串 \"%s\" 中提取出数字\n", str);
        else {
            printf("从字符串 \"%s\" 中提取出的数字是:", str);
            *num_end = '\0'; // 截断字符串,只保留开头的数字部分
            puts(str);
        }
    }

    return 0;
}
请输入字符串:2.71828
从字符串 "2.71828" 中提取出的数字是:2.71828

请输入字符串:1024
从字符串 "1024" 中提取出的数字是:1024

请输入字符串:6.626070e-19[[
从字符串 "6.626070e-19[[" 中提取出的数字是:6.626070e-19

请输入字符串:1231__sf
从字符串 "1231__sf" 中提取出的数字是:1231

请输入字符串:78E6...
从字符串 "78E6..." 中提取出的数字是:78E6

请输入字符串:01
从字符串 "01" 中提取出的数字是:0  # 没有识别成 01(参照上述给出的识别标准)

请输入字符串:abcd
无法从字符串 "abcd" 中提取出数字

代码实现

可以用有穷状态自动机 DFA 来描述和解决这个问题

S0
S0
I0
I0
I1
I1
I2
I2
1~9
1~9
1~9
1~9
0~9
0~9
0~9
0~9
0
0
0
0
.
.
.
.
.
.
F1
F1
0~9
0~9
0~9
0~9
E0
E0
e
e
e
e
e
e
e
e
E1
E1
+|-
+|-
E2
E2
0~9
0~9
0~9
0~9
0~9
0~9
I3
I3
F0
F0
Text is not SVG - cannot display

初态:S0

终态:I1I2I3F2E2

  • . 表示读入小数点
  • - 表示读入负号
  • +|- 表示读入正号或负号
  • 0 表示读入数字 0
  • 1~9 表示读入 1 ~ 9 中的任一个数字
  • 0~9 表示读入 0 ~ 9 中的任一个数字
  • e 表示读入字母 e 或 E

基于【goto】实现 DFA

char *extract_number(const char *str) {
    const char *p = str;
    const char *r = NULL;

    if ('1' <= *p && *p <= '9') goto I1;
    if (*p == '-') goto I0;
    if (*p == '0') goto I3;
    goto ED;
I0: ++p;
    if ('1' <= *p && *p <= '9') goto I1;
    if (*p == '0') goto I3;
    goto ED;
I1: r = ++p;
    if ('0' <= *p && *p <= '9') goto I2;
    if ('.' == *p) goto F0;
    if ('e' == *p || 'E' == *p) goto E0;
    goto ED;
I2: r = ++p;
    if ('0' <= *p && *p <= '9') goto I2;
    if ('.' == *p) goto F0;
    if ('e' == *p || 'E' == *p) goto E0;
    goto ED;
I3: r = ++p;
    if ('.' == *p) goto F0;
    if ('e' == *p || 'E' == *p) goto E0;
    goto ED;
F0: ++p;
    if ('0' <= *p && *p <= '9') goto F1;
    goto ED;
F1: r = ++p;
    if ('0' <= *p && *p <= '9') goto F1;
    if ('e' == *p) goto E0;
    goto ED;
E0: ++p;
    if ('0' <= *p && *p <= '9') goto E2;
    if (*p == '-' || *p == '+') goto E1;
    goto ED;
E1: ++p;
    if ('0' <= *p && *p <= '9') goto E2;
    goto ED;
E2: r = ++p;
    if ('0' <= *p && *p <= '9') goto E2;
    goto ED;
ED: return (char *)r;
}

基于【if】【while】实现 DFA

char *extract_number(const char *str) {
    const char *p = str;
    const char *r = NULL;

    /* 整数部分 */
    if (*p == '-')
        ++p;
    if ('1' <= *p && *p <= '9') {
        r = ++p;
        while ('0' <= *p && *p <= '9')
            r = ++p;
    }
    else if (*p == '0')
        r = ++p;
    else
        goto end;

    /* 小数部分 */
    if (*p == '.') {
        ++p;
        if ('0' <= *p && *p <= '9')
            r = ++p;
        else
            goto end;
        while ('0' <= *p && *p <= '9')
            r = ++p;
    }

    /* 指数部分 */
    if (*p == 'e' || *p == 'E') {
        ++p;
        if (*p == '-' || *p == '+')
            ++p;
        if ('0' <= *p && *p <= '9') {
            r = ++p;
            while ('0' <= *p && *p <= '9')
                r = ++p;
        }
    }

end:
    return (char *)r;
}

基于【状态转换表】实现 DFA

char *extract_number(const char *str) {

    enum state {
    // --------------------------------------------
    // 状态集
    // --------------------------------------------
        S0,             // start    开始
        I0, I1, I2, I3, // integer  正在识别整数部分
        F0, F1,         // fraction 正在识别小数部分
        E0, E1, E2,     // exponent 正在识别指数部分
        ED,             // end      结束,退出状态机
    // --------------------------------------------
    // 初态:S0        终态:I1, I2, I3, F1, E2
    };

    enum symbol {
    // --------------------------------
    // 输入符号集
    // --------------------------------
        PLUS,   // +   | 正号
        MINUS,  // -   | 负号
        POINT,  // .   | 小数点
        ZERO,   // 0   | 数字 0
        DIGIT,  // 1~9 | 数字 1~9
        EXP,    // e E | 指数符号 e 或 E
        OTHERS, //     | 其他符号
    // --------------------------------
        SYMBOL_SET_SIZE
    };

    static enum state transition_table[]  = {
    // ------------------------------------
    // 状态转换表
    // ------------------------------------
    //        +   -   .   0  1~9 eE  others
    /* S0 */ ED, I0, ED, I3, I1, ED, ED,
    /* I0 */ ED, ED, ED, I3, I1, ED, ED,
    /* I1 */ ED, ED, F0, I2, I2, E0, ED,
    /* I2 */ ED, ED, F0, I2, I2, E0, ED,
    /* I3 */ ED, ED, F0, ED, ED, E0, ED,
    /* F0 */ ED, ED, ED, F1, F1, ED, ED,
    /* F1 */ ED, ED, ED, F1, F1, E0, ED,
    /* E0 */ E1, E1, ED, E2, E2, ED, ED,
    /* E1 */ ED, ED, ED, E2, E2, ED, ED,
    /* E2 */ ED, ED, ED, E2, E2, ED, ED,
    // ------------------------------------
    };

    const char *p = str;
    const char *r = NULL;

    /* DFA 开始 */
    enum state state = S0;
    for (;;) {
        /* 读入字符,判断字符属于符号集中的哪个符号 */
        enum symbol input;
        switch (*p++) {
            case '1': case '2': case '3': case '4':
            case '5': case '6': case '7': case '8':
            case '9': input = DIGIT;  break;
            case '0': input = ZERO;   break;
            case '+': input = PLUS;   break;
            case '-': input = MINUS;  break;
            case '.': input = POINT;  break;
            case 'e':
            case 'E': input = EXP;    break;
            default:  input = OTHERS; break;
        }

        /* 状态转移 */
        state = transition_table[SYMBOL_SET_SIZE * state + input];

        /* 每到一次终态则记录一次位置 */
        switch (state) {
            case I1:
            case I2:
            case I3:
            case F1:
            case E2: r = p;
            default:;
        }

        /* 直到新读入的字符与已扫描的部分不再构成合法的数字 */
        if (ED == state)
            break;
    }

    return (char *)r;
}

在定义 enum symbol 时,往末尾添加一项 SYMBOL_SET_SIZE,即可得到输入符号集的大小,也就是状态转换表的列数

状态转换表的行列索引的顺序要与 enum stateenum symbol 的顺序保持一致


评论