Code Golf:数学表达式评估器(尊重 PEMDAS)

2023-12-01

我挑战你编写一个遵守 PEMDAS(运算顺序:括号、求幂、乘法、除法、加法、减法)的数学表达式求值器,而不使用正则表达式、预先存在的类似“Eval()”的函数、解析库, ETC。

我看到了一项关于 SO 的现有评估员挑战(here),但是那个特别需要从左到右的评估。

输入和输出示例:

"-1^(-3*4/-6)" -> "1"

"-2^(2^(4-1))" -> "256"

"2*6/4^2*4/3" -> "1"

我用 C# 编写了一个评估器,但想看看它与那些使用自己选择的语言的聪明程序员的评估器相比有多糟糕。

Related:

Code Golf:评估数学表达式

澄清:

  1. 让我们将其设为一个接受字符串参数并返回字符串结果的函数。

  2. 至于为什么没有正则表达式,那是为了公平竞争。我认为“最紧凑的正则表达式”应该有一个单独的挑战。

  3. 使用 StrToFloat() 是可以接受的。通过“解析库”,我的意思是排除通用语法解析器之类的东西,也是为了公平竞争。

  4. 支持浮标。

  5. 支持括号、指数和四个算术运算符。

  6. 给予乘法和除法同等的优先级。

  7. 加法和减法同等优先。

  8. 为简单起见,您可以假设所有输入都是格式正确的。

  9. 对于您的函数是否接受“.1”或“1e3”作为有效数字,我没有偏好,但接受它们将为您赢得布朗尼积分。 ;)

  10. 对于被零除的情况,您也许可以返回“NaN”(假设您希望实现错误处理)。


C(465 个字符)

#define F for(i=0;P-8;i+=2)
#define V t[i
#define P V+1]
#define S V+2]),K(&L,4),i-=2)
#define L V-2]
K(double*t,int i){for(*++t=4;*t-8;*++t=V])*++t=V];}M(double*t){int i,p,b;
F if(!P)for(p=1,b=i;i+=2,p;)P?P-1||--p||(P=8,M(t+b+2),K(t+b,i-b),i=b):++p;
F P-6||(L=pow(L,S;F P-2&&P-7||(L*=(P-7?V+2]:1/S;F P-4&&(L+=(P-5?V+2]:-S;
F L=V];}E(char*s,char*r){double t[99];char*e,i=2,z=0;for(;*s;i+=2)V]=
strtod(s,&e),P=z=e-s&&z-4&&z-1?s=e,4:*s++&7;P=8;M(t+2);sprintf(r,"%g",*t);}

前五个换行符是必需的,其余的只是为了可读性。我已将前五个换行符分别算作一个字符。如果你想以行数来衡量,在我删除所有空白之前,它是 28 行,但这是一个毫无意义的数字。它可以是从 6 行到 100 万行的任何内容,具体取决于我如何格式化它。

入口点是E()(“评估”)。第一个参数是输入字符串,第二个参数指向输出字符串,并且必须由调用者分配(按照通常的 C 标准)。它最多可以处理 47 个令牌,其中令牌可以是运算符(“之一”+-*/^()") 或浮点数。一元符号运算符不计为单独的标记。

这段代码大致基于我多年前作为练习完成的一个项目。我删除了所有错误处理和空白跳过,并使用高尔夫技术对其进行了重新设计。下面是 28 行,具有足够的格式,我能够write它,但可能还不足以read它。你会想要#include <stdlib.h>, <stdio.h>, and <math.h>(或参见底部的注释)。

请参阅代码后了解其工作原理的说明。

#define F for(i=0;P-8;i+=2)
#define V t[i
#define P V+1]
#define S V+2]),K(&L,4),i-=2)
#define L V-2]
K(double*t,int i){
    for(*++t=4;*t-8;*++t=V])
        *++t=V];
}
M(double*t){
    int i,p,b;
    F if(!P)
        for(p=1,b=i;i+=2,p;)
            P?P-1||--p||(P=8,M(t+b+2),K(t+b,i-b),i=b):++p;
    F P-6||(L=pow(L,S;
    F P-2&&P-7||(L*=(P-7?V+2]:1/S;
    F P-4&&(L+=(P-5?V+2]:-S;
    F L=V];
}
E(char*s,char*r){
    double t[99];
    char*e,i=2,z=0;
    for(;*s;i+=2)
        V]=strtod(s,&e),P=z=e-s&&z-4&&z-1?s=e,4:*s++&7;
    P=8;
    M(t+2);
    sprintf(r,"%g",*t);
}

第一步是标记化。双精度数组包含每个标记的两个值,一个运算符 (P, 因为O看起来太像零)和一个值(V)。这种标记化是在for循环进入E()。它还处理任何一元+ and -运算符,将它们合并到常量中。

令牌数组的“operator”字段可以具有以下值之一:

0: (
1: )
2: *
3: +
4: 浮点常数值
5: -
6: ^
7: /
8: 令牌字符串结尾

该方案主要源自丹尼尔·马丁,他注意到本次挑战赛中每个运算符的 ASCII 表示形式的最后 3 位都是唯一的。

未压缩版本E()看起来像这样:

void Evaluate(char *expression, char *result){
    double tokenList[99];
    char *parseEnd;
    int i = 2, prevOperator = 0;
    /* i must start at 2, because the EvalTokens will write before the
     * beginning of the array.  This is to allow overwriting an opening
     * parenthesis with the value of the subexpression. */
    for(; *expression != 0; i += 2){
        /* try to parse a constant floating-point value */
        tokenList[i] = strtod(expression, &parseEnd);

        /* explanation below code */
        if(parseEnd != expression && prevOperator != 4/*constant*/ &&
           prevOperator != 1/*close paren*/){
            expression = parseEnd;
            prevOperator = tokenList[i + 1] = 4/*constant*/;
        }else{
            /* it's an operator */
            prevOperator = tokenList[i + 1] = *expression & 7;
            expression++;
        }
    }

    /* done parsing, add end-of-token-string operator */
    tokenList[i + 1] = 8/*end*/

    /* Evaluate the expression in the token list */
    EvalTokens(tokenList + 2); /* remember the offset by 2 above? */

    sprintf(result, "%g", tokenList[0]/* result ends up in first value */);
}

由于我们保证输入有效,因此解析失败的唯一原因是下一个标记是运算符。如果发生这种情况,则parseEnd指针将与以下内容相同:tokenStart。我们还必须处理解析的情况成功了,但我们真正想要的是一个运算符。这种情况会发生在加法和减法运算符上,除非直接跟在符号运算符后面。换句话说,给定表达式“4-6“,我们想将其解析为{4, -, 6},而不是作为{4, -6}。另一方面,鉴于“4+-6“,我们应该将其解析为{4, +, -6}。解决方案非常简单。如果解析失败OR前面的标记是一个常量或一个右括号(实际上是一个将计算为常量的子表达式),那么当前标记是一个运算符,否则它是一个常量。

标记化完成后,通过调用完成计算和折叠M(),它首先查找任何匹配的括号对,并通过递归调用自身来处理其中包含的子表达式。然后它处理运算符,首先是求幂,然后是乘法和除法,最后是加法和减法。由于需要格式良好的输入(如挑战中所指定),因此它不会显式检查加法运算符,因为它是处理所有其他运算符后的最后一个合法运算符。

缺少高尔夫压缩的计算函数将如下所示:

void EvalTokens(double *tokenList){
    int i, parenLevel, parenStart;

    for(i = 0; tokenList[i + 1] != 8/*end*/; i+= 2)
        if(tokenList[i + 1] == 0/*open paren*/)
            for(parenLevel = 1, parenStart = i; i += 2, parenLevel > 0){
                if(tokenList[i + 1] == 0/*another open paren*/)
                    parenLevel++;
                else if(tokenList[i + 1] == 1/*close paren*/)
                    if(--parenLevel == 0){
                        /* make this a temporary end of list */
                        tokenList[i + 1] = 8;
                        /* recursively handle the subexpression */
                        EvalTokens(tokenList + parenStart + 2);
                        /* fold the subexpression out */
                        FoldTokens(tokenList + parenStart, i - parenStart);
                        /* bring i back to where the folded value of the
                         * subexpression is now */
                        i = parenStart;
                    }
            }

    for(i = 0; tokenList[i + 1] != 8/*end*/; i+= 2)
        if(tokenList[i + 1] == 6/*exponentiation operator (^)*/){
            tokenList[i - 2] = pow(tokenList[i - 2], tokenList[i + 2]);
            FoldTokens(tokenList + i - 2, 4);
            i -= 2;
        }
    for(i = 0; tokenList[i + 1] != 8/*end*/; i+= 2)
        if(tokenList[i + 1] == 2/*multiplication operator (*)*/ ||
           tokenList[i + 1] == 7/*division operator (/)*/){
            tokenList[i - 2] *=
                (tokenList[i + 1] == 2 ?
                    tokenList[i + 2] :
                    1 / tokenList[i + 2]);
            FoldTokens(tokenList + i - 2, 4);
            i -= 2;
        }
    for(i = 0; tokenList[i + 1] != 8/*end*/; i+= 2)
        if(tokenList[i + 1] != 4/*constant*/){
            tokenList[i - 2] +=
                (tokenList[i + 1] == 3 ?
                    tokenList[i + 2] :
                    -tokenList[i + 2]);
            FoldTokens(tokenList + i - 2, 4);
            i -= 2;
        }
    tokenList[-2] = tokenList[0];
    /* the compressed code does the above in a loop, equivalent to:
     *
     * for(i = 0; tokenList[i + 1] != 8; i+= 2)
     *     tokenList[i - 2] = tokenList[i];
     *
     * This loop will actually only iterate once, and thanks to the
     * liberal use of macros, is shorter. */
}

Some amount of compression would probably make this function easier to read.

一旦执行操作,操作数和运算符就会从标记列表中折叠出来:K()(通过宏调用S)。运算结果保留为常量,代替折叠表达式。因此,最终结果留在标记数组的开头,因此当控制权返回到E(),它只是将其打印到字符串中,利用数组中的第一个值是令牌的值字段这一事实。

这个电话给FoldTokens()发生在操作之后(^, *, /, +, or -) 已执行,或在处理子表达式(用括号括起来)之后。这FoldTokens()例程确保结果值具有正确的运算符类型 (4),然后复制子表达式的较大表达式的其余部分。例如,当表达式“2+6*4+1“ 已处理,EvalTokens()首先计算6*4,将结果保留在6 (2+24*4+1). FoldTokens()然后删除子表达式的其余部分“24*4”,离开2+24+1.

void FoldTokens(double *tokenList, int offset){
    tokenList++;
    tokenList[0] = 4; // force value to constant

    while(tokenList[0] != 8/*end of token string*/){
        tokenList[0] = tokenList[offset];
        tokenList[1] = tokenList[offset + 1];
        tokenList += 2;
    }
}

就是这样。宏只是用来替换常见操作,其他一切都只是上述内容的高尔夫压缩。


strager坚持认为代码应该包括#include语句,因为如果没有正确的前向声明,它将无法正确运行strtod and pow和功能。由于挑战只要求一个函数,而不是一个完整的程序,因此我认为这不应该是必需的。但是,可以通过添加以下代码以最小的成本添加前向声明:

#define D double
D strtod(),pow();

然后我将替换“的所有实例double” 在代码中带有“D”。这会在代码中添加 19 个字符,使总数达到 484 个。另一方面,我也可以将函数转换为返回双精度值而不是字符串,就像他一样,这会删除 15 个字符,从而更改E()函数为此:

D E(char*s){
    D t[99];
    char*e,i=2,z=0;
    for(;*s;i+=2)
        V]=strtod(s,&e),P=z=e-s&&z-4&&z-1?s=e,4:*s++&7;
    P=8;
    M(t+2);
    return*t;
}

这将使总代码大小为 469 个字符(如果没有前向声明,则为 452 个字符)strtod and pow,但随着D宏)。甚至可以通过要求调用者传入一个指向 double 作为返回值的指针来修剪 1 个字符:

E(char*s,D*r){
    D t[99];
    char*e,i=2,z=0;
    for(;*s;i+=2)
        V=strtod(s,&e),P=z=e-s&&z-4&&z-1?s=e,4:*s++&7;
    P=8;
    M(t+2);
    *r=*t;
}

我将让读者决定哪个版本合适。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Code Golf:数学表达式评估器(尊重 PEMDAS) 的相关文章

  • 将人类日期(当地时间 GMT)转​​换为日期

    我正在服务器上工作 服务器正在向我发送 GMT 本地日期的日期 例如Fri Jun 22 09 29 29 NPT 2018在字符串格式上 我将其转换为日期 如下所示 SimpleDateFormat simpleDateFormat ne
  • 将 Java 字符串转换为 sql.Timestamp

    收到以下格式的字符串 YYYY MM DD HH MM SS NNNNNN 时间戳来自 DB2 数据库 我需要将其解析为 java sql Timestamp 并且不丢失任何精度 到目前为止 我一直无法找到现有的代码来解析远至微秒的数据 S
  • 计算两点之间的最短路线

    过去几周我一直在开发一款多人 HTML5 游戏 使用nodejs and websockets 我已经被这个问题困扰了一段时间 想象一下 我用数组实现了这个平铺地图 如下所示 1 or 棕色瓷砖 路上有障碍物 玩家无法通过 0 or 绿色瓷
  • 有 JavaScript 的微积分库吗? [关闭]

    Closed 此问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 有人知道 JavaScript 的微积分库吗 我做了一些谷歌搜索 但没有想出任何东西 我申请了 Wolf
  • 如何使用 BeautifulSoup 从表中选择特定行?

    So I have a question related to a previous question but I realized I needed to go one level more to get an 11 digit NDC
  • 使用多个可选模式时顺序的重要性

    可选模式的顺序如何DateTimeFormatter影响解析操作吗 我正在运行这个程序 想知道为什么最后一行抛出异常而不是前三行 public static void main String args String p1 EEEE E dd
  • 为什么 C# Math.Ceiling 向下舍入?

    我今天过得很艰难 但有些事情不太对劲 在我的 C 代码中 我有这样的内容 Math Ceiling decimal this TotalRecordCount this PageSize Where int TotalRecordCount
  • 使用按位运算符相乘

    我想知道如何使用按位运算符将一系列二进制位相乘 但是 我有兴趣这样做来查找二进制值的十进制小数值 这是我正在尝试做的一个例子 假设 1010010 我想使用每个单独的位 以便将其计算为 1 2 1 0 2 2 1 2 3 0 2 4 虽然我
  • 重新创建 CSS3 过渡三次贝塞尔曲线

    在 CSS3 过渡中 您可以将计时函数指定为 cubic bezier 0 25 0 3 0 8 1 0 在该字符串中 您仅指定曲线上点 P1 和 P2 的 XY 因为 P0 和 P3 始终分别为 0 0 0 0 和 1 0 1 0 根据苹
  • 从 1 到 20 亿,像 (23,29) 这样相差 6 的连续素数对的数量

    如何在考虑时间复杂度的情况下从 1 到 20 亿 使用任何编程语言且不使用任何外部库 找到像 23 29 这样相差 6 的连续素数对的数量 尝试过埃拉托色尼筛 但获得连续素数是一个挑战 使用了生成器 但时间复杂度非常高 代码是 def ge
  • 将 z 分数转换为百分比的函数

    谷歌不想提供帮助 我能够计算 z 分数 并且我们正在尝试生成一个函数 给定 z 分数 可以得出正态分布中低于该 z 分数的人口百分比 我能找到的只是对百分比表的 z 分数的引用 有什么指点吗 Is it 这个 z 分数 链接 http en
  • 在 2D 中将一个点旋转另一个点

    我想知道当一个点相对于另一个点旋转一定角度时如何计算出新的坐标 我有一个块箭头 想要将其相对于箭头底部中间的点旋转角度 theta 这是允许我在两个屏幕控件之间绘制多边形所必需的 我无法使用和旋转图像 从我到目前为止所考虑的情况来看 使问题
  • 如果数字小于 10,则显示前导零 [重复]

    这个问题在这里已经有答案了 可能的重复 JavaScript 相当于 printf string format https stackoverflow com questions 610406 javascript equivalent t
  • 自动将 JSON 对象映射到 Ruby 中的实例变量

    我希望能够自动将 JSON 对象解析为实例变量 例如 使用此 JSON require httparty json HTTParty get http api dribbble com players simplebits gt shots
  • PHP 负面因素不断增加

    我这里有这个代码 remaining 0 foreach clientArrayInvoice as key gt row remaining remaining row total 它的作用是 它获取总计值并将它们相加 但是当我有负值时
  • 如何将Excel中的每个条目转换为一行“矩阵”表

    我有类似的东西 1 2 3 a x o x b x x o c o o o 并想将其转换成像这样的线 1 a x 1 b x 1 c x 2 a o 2 b x 2 c o 3 a x 3 b o 3 c o 通过使用Excel文档中的公式
  • 用于解析差异的 PHP 类

    我正在编写一个 PHP 脚本 需要解释 Git 创建的 Diff 文件 如果我想解析 Diff 文件并基本上以完全不同的格式打印它 我应该如何进行 我遇到过Text DiffPEAR 库 但该库仅创建 Diff 本身 或者更确切地说 它只需
  • h264 参考帧

    我正在寻找一种在 h264 流中查找参考帧的算法 我在不同的解决方案中看到的最常见的方法是查找访问单元分隔符和 IDR 类型的 NAL 不幸的是 我检查的大多数流没有 IDR 类型的 NAL 我将不胜感激的帮助 问候 雅采克 H264 帧由
  • 使用 python 将 bibtex 文件转换为 html (也许是 pybtex?)

    您好 我想解析 bibtex 出版物文件并对特定字段 例如年份 进行排序并过滤某些内容 然后将其放在网站上 我遇到了 pybtex 它可以读取和解析 bibtex 文件 但它基本上没有记录 我不知道如何对条目进行排序 pybtex 是可行的
  • 使用 isdigit 表示浮点数?

    a raw input How much is 1 share in that company while not a isdigit print You need to write a number n a raw input How m

随机推荐

  • Hive 上的 Spark SQL 查询执行

    我是 Spark SQL 新手 但了解 Hive 查询执行框架 我想了解spark如何执行sql查询 技术说明 如果我按照命令开火 val sqlContext new org apache spark sql hive HiveConte
  • 如何使 setInterval 在一段时间或多次操作后停止?

    我用 jQuery 创建了一个 改变单词 的循环 通过使用此答案中的代码 jQuery 查找单词并每隔几秒更改一次 一段时间后如何停止 是说 60 秒后还是循环结束后 function List your words here var wo
  • 格式化 pandas 中的数字

    对于 pandas DataFrame df min max mean a 0 0 2 300000e 04 6 450098e 02 b 0 0 1 370000e 05 1 651754e 03 c 218 0 1 221550e 10
  • 如何使用 P/Invoke 在 C# 中返回列表?

    我正在开发一个小项目 我使用 P Invoke 并希望在 C 中返回以下内容 public class std list
  • 下面的格式说明符在做什么?

    else printf 3hho data 我无法在网上或通过阅读 C 编程语言书籍找到有关如何破译它的信息 我在下面的代码片段中看到了它 该代码尝试在 telnet 协议中执行密码嗅探 if pktlen sizeof struct ip
  • 使用 hibernate 从数据库获取下一个序列值

    我有一个实体 该实体具有必须从序列设置的非 ID 字段 目前 我获取序列的第一个值 将其存储在客户端 然后根据该值进行计算 然而 我正在寻找一种 更好 的方法来做到这一点 我已经实现了一种获取下一个序列值的方法 public Long ge
  • 通过 Dexterity 在字段集之间移动字段

    在 Archetypes 中 为了将字段从字段集 或模式 移动到另一个字段集 或模式 我们可以执行以下操作 schema creators schemata default 然而 我并没有使用敏捷来实现同样的目标 我尝试过使用表单提示 前任
  • 如何使用 Microsoft Graphs 访问共享邮箱

    是否可以使用图表访问共享邮箱 我想使用图表访问共享邮箱邮件文件夹 只需将其视为任何其他用户即可 https graph microsoft com v1 0 users 电子邮件受保护 消息 确保您设置了正确的权限 Mail Read Sh
  • Django:将值从模板传递到视图

    我遇到过这种情况 单击 html 提交按钮 我调用views stream response哪个 激活 views stream response generator哪个 激活 流 py并返回一个流式Http响应我每秒都会看到一个渐进的数字
  • 如何从 xamarin.forms 中的应用程序打开设置?

    我正在研究 xamarin forms 仅在android中面临以下问题 当我的应用程序启动时 它会检查我的 GPS 位置是否打开或关闭 要检查 GPS 位置的开启或关闭 我正在使用依赖服务 public static bool Check
  • 合并 R 中的唯一值

    这是示例数据 set seed 123 data1 lt data frame id1 rep 1 5 each 2 nam1 rnorm 5 1 data2 lt data frame id2 rep 3 12 each 2 nam2 r
  • 您最喜欢的使用 Bash 的命令行技巧是什么? [关闭]

    很难说出这里问的是什么 这个问题模棱两可 含糊不清 不完整 过于宽泛或言辞激烈 无法以目前的形式合理回答 如需帮助澄清此问题以便重新打开 访问帮助中心 Locked 这个问题及其答案是locked因为这个问题是题外话 但却具有历史意义 目前
  • 如何在选项卡之间切换时停止执行 AsyncTask,同时保留之前的内容

    让我解释一下我的问题 假设我有三个选项卡 片段 ab1 片段 ab2 片段 ab3 现在我在 FragmentTab1 中有列表视图 这里我使用AsyncTask加载数据OnCreateView 数据正在完美加载 现在 当我查看详细信息并再
  • asp.net中缓存过期时的回调

    有谁知道当缓存过期时如何在 ASP NET 中运行函数的教程或示例 我读过有关缓存过期时进行的回调的信息 但我没有找到任何示例 我需要这个来做网站 它需要在每天的确切时间执行一个函数 hhh3112 当缓存过期时 您可以使用回调 你能再解释
  • 以24小时制显示日期

    我正在使用简单的应用程序 我可以在其中获取最新信息DateTime并将其转换为24 hour format Code String DATE yyyy MM dd hh mm ss yyyy MM dd hh mm ss String DA
  • war webapp 中 Tomcat 服务器绝对文件访问

    我有一个 Spring web 应用程序 war文件已上传至 Tomcat 服务器 大多数基本功能都按预期工作 页面视图和表单提交 我现在的问题是我的 web 应用程序需要读取和写入文件 而我对如何实现这一点一无所知 文件 I O 返回ja
  • jQuery绑定粘贴事件,如何获取粘贴的内容

    我有一个 jquery token tagit 插件 我想绑定到粘贴事件以正确添加项目 我可以像这样绑定到粘贴事件 bind paste paste input function paste input e console log e re
  • 使用简单的注入器注册 Web API 控制器的子集

    我正在手动注册项目的 Web API 控制器的子集 container Register typeof ILGTWebApiController controllerType Lifestyle Transient 工作正常 但是 当我运行
  • 从 python 脚本调用 scrapy 不创建 JSON 输出文件

    这是我用来调用 scrapy 的 python 脚本 答案是 从脚本中抓取的 Scrapy 总是在抓取后阻止脚本执行 def stop reactor reactor stop dispatcher connect stop reactor
  • Code Golf:数学表达式评估器(尊重 PEMDAS)

    Locked 这个问题及其答案是locked因为这个问题是题外话 但却具有历史意义 目前不接受新的答案或互动 我挑战你编写一个遵守 PEMDAS 运算顺序 括号 求幂 乘法 除法 加法 减法 的数学表达式求值器 而不使用正则表达式 预先存在