65.9K
CodeProject 正在变化。 阅读更多。
Home

程序员日记 -- Marc 的船厂 Bug

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.35/5 (43投票s)

2003年2月15日

6分钟阅读

viewsIcon

109768

Marc 在讨论一个涉及 atof 函数的 Bug 时全盘托出。

引言

不,atof 函数本身没有问题。有问题的是我使用它的代码。我想这个小故事对你们中的一些人来说会很有趣,所以我决定再写一篇文章,增加我的文章数量,并写下这篇小小的阐述,向你们,亲爱的读者,暴露我自己,任由你们嘲笑和哄笑。

现在,CP 上有很多优秀的文章,我当然也贡献了几篇,但似乎关于我们日常的挣扎和我们所做的愚蠢事情的文章很少(如果有的话——我肯定没找到,但话说回来,我也没怎么努力找)。所以我又一次在 CP 上树立了先例,写了一些别人因为羞愧、害怕或聪明而不愿写的东西。在字里行间,你可能会发现触动你灵魂的东西(或者让你乘坐“陶瓷巴士”)。

有些 Bug 实际上并非那么愚蠢,它们说明了即使有了最好的单元测试,**我们也无法测试所有可能的情况**。这是一个关于我如何搞砸一段代码的故事,这段代码正常工作了一个月,直到有一天我们收到了一家供应商的账单,金额对我软件来说太大了,无法处理。

铺垫

有些人知道我在“征税州”康涅狄格州为两个船厂做一些合同工作。康涅狄格州万物皆税,而且一切都要收费——即使是海滩。事实上,刚刚连任的州长 Rowland 因为明年的一个项目 15 亿美元的巨额赤字,正在关闭该州一半的机动车管理局办公室。现在,这些都与我的 Bug 或这篇文章无关,除了那个巨额金额的一个特殊特征。但我说得太快了。

其中一个会计功能需要将供应商的发票与采购订单进行核对,并将运费录入系统。然后,PO 就可以关闭,客户将被收取零件费用和船厂产生的任何运费。那么,你可能会问,运费是如何分配的?嗯,运费是根据每件商品的数量*成本占整个发票金额的百分比来分配的。没有其他方法可以做到,因为我们不知道物品的重量,而运费实际上是按重量收费的。所以,这意味着购买 5000 美元雷达的家伙会承担大部分运费,而另一个人在同一张发票上购买一个 500 磅的铅压载块,价格只有 5 美元。没有人说过生活是公平的。而且没有人说过我的例子与现实有任何关系。

现在,请注意这张截图,它是在问题发生后进行的检查,对于这个零件(顺便说一句,这是一个 12V 的泵),供应商成本是 162.88 美元,但我的软件计算出,在计入 37.56 美元的运费后,该零件的实际成本高达 1304.86 美元!

我的天。这可不好。船厂为每个库存项目维护一个“调整后成本”,这是零件实际价格加上运费的移动平均值。每当购买一次零件时,移动平均值就会发挥作用。调整后成本将按当前成本的 4/5 加上新成本的 1/5 来重新计算。这样,我们就可以慢慢地修改我们的库存成本。现在,对于这个 162 美元的零件,我在库存价值中增加了 1300 多美元。

不用说,我们的会计**非常不高兴**。对我。

GUI 和数据采集

那么,让我们来追踪一下这是如何发生的。

首先,显示屏幕上信息的 GUI 看起来是这样的(这是其中的一部分)

GUI:ReceiveCheckTab
dlg nosize true
dlg caption "Process Vendor Invoice"
font ("MS Sans Serif", 8)
end.
STATIC s1 at (0, 20) size (100, 15) font 
  ("MS Sans Serif", 8, B) caption "Select Part:" end.
LISTCONTROL lcCheckItems at (0, 35) size (465, 120)
    storage rcvCheckIdx
    list storage rcvCheckList
    header ("OK?":40C,
                ID:0,
                partID:0,
                "WO/Dept":70,
                "Vend. Part #":80,
                "Qty":50,
                "Cost $":65R,
                "Total $":70R,
                AdjustedCost:0,
                MSRPCost:0,
                MarkupOption:0,
                Markup:0,
                ...)
    with options (grid editable)
    on selection ScriptMgr.RunMacro(dp_macroName, SelectRcvCheckItem)
    on double click ScriptMgr.RunMacro(dp_macroName, RcvShowPartInfo2) end.
...
BUTTON btnClosePO at (480, 220) size (100, 20) caption "Close PO"
        on selection ScriptMgr.RunMacro(dp_macroName, ClosePO) end.
...
GUIEND

这看起来不像你见过的任何 GUI,对吧?那是因为它是我的 C++/MFC 应用程序自动化层的一部分,我用于所有 C++ 项目。我相信聪明的读者可以根据截图中的 GUI 来推断出 LISTCONTROL 的规范。

现在,这是加载 rcvCheckList 矩阵的数据库查询

DBMgr.QueryMultiRow(dp_DB, MyDB, "select
        a.CHECKED,
    a.ID,
    a.PARTNUM_ID,
    g.WO_NUMBER,
    f.VENDOR_PARTNUM,
    a.QTY,
    a.VENDOR_COST,
    VAL(a.QTY)*VAL(a.VENDOR_COST),
    b.ADJUSTED_COST,
    b.MSRP_COST,
    b.MARKUP_OPTION,
    b.MARKUP,
    a.PROBLEM_FLAG,
    f.ID,
    g.WO_NUMBER,
    a.COMMENT,
    0,
    'per '+a.BASE_QTY+' '+h.NAME,
    'Stocking Unit is per '+i.NAME,
    h.ID,
    i.ID,
    0,
    0,
    0
from
    PO_ITEM a,
    INVENTORY b,
    VENDOR c,
    PURCHASE_ORDER d,
    VENDOR_PART f,
    WORKORDER g,
    UNIT h,
    UNIT i
where
    b.ID=a.PARTNUM_ID and d.ID=a.PO_ID and c.ID=d.VENDOR_ID and d.ID={rcvID} and
        g.ID=a.WO_ID and f.PARTNUM_ID=a.PARTNUM_ID 
        and VAL(f.BASE_QTY)=VAL(a.BASE_QTY) and
        f.VENDOR_ID=d.VENDOR_ID and a.UNIT_ID=f.UNIT_ID and
    h.ID=f.UNIT_ID and i.ID=b.STOCKING_UNIT_ID
order
    by a.ID", rcvCheckList)

后面是一些数据和列表控件的调整

...
DataMatrix.Iterate(dp_iterateMatrix, rcvCheckList, i, AdjustCheckList, 0)
Number.CommaFormat(dp_formatNumber, rcvCheckTotal, {rcvCheckTotal}, %.2lf)
WinMgr.SetListControlEditStyle(dp_EditStyle, 
      ReceiveCheckTab, lcCheckItems, 0, YESNO)
WinMgr.UpdateAllControls(dp_viewName, ReceivePartsTab)
...
MACRO:AdjustCheckList
Math.RPN(dp_RPN, "{rcvCheckTotal} {rcvCheckList(7, {i})} + rcvCheckTotal STO")
Number.Format(dp_formatNumber, rcvCheckList(6, {i}), {rcvCheckList(6, {i})}, %.4lf)
Number.Format(dp_formatNumber, rcvCheckList(7, {i}), {rcvCheckList(7, {i})}, %.2lf)
Number.Format(dp_formatNumber, rcvCheckList(8, {i}), {rcvCheckList(8, {i})}, %.4lf)
Number.Format(dp_formatNumber, rcvCheckList(9, {i}), {rcvCheckList(9, {i})}, %.4lf)
Number.Format(dp_formatNumber, 
  rcvCheckList(10, {i}), {rcvCheckList(10, {i})}, %.4lf)
Number.Format(dp_formatNumber, 
  rcvCheckList(11, {i}), {rcvCheckList(11, {i})}, %.4lf)
end.
MACROEND

对于不熟悉我的 AAL 脚本语言(你们所有人!)的人来说,大括号 {} 中的内容会返回它们的值。例如,{rcvCheckList(6, {i})} 返回 rcvCheckList 矩阵的第 i 行的第 6 列的值。“RPN”表示“逆波兰表示法”,这是一种基于堆栈的处理器,由惠普计算器而闻名。

现在,你,这位敏锐的读者,是否已经发现了我的愚蠢错误,并且正在嘲笑我???如果没有,请继续阅读。

关闭 PO

当按下“关闭 PO”按钮时,程序开始执行以下脚本

MACRO:ClosePO
ScriptMgr.VerifySelection(dp_verifySelection, 
   ClosePO, {rcvID}, -1, "Please select a PO.")
DBMgr.QuerySingleRow(dp_DB, MyDB, 
  "select FREIGHT <totalFreight> from PURCHASE_ORDER where ID={rcvID}")
DataMatrix.Iterate(dp_iterateMatrix, 
  rcvCheckList, i, GetLineItemFreight, 0)
...

它会验证是否有 PO 可以操作。然后获取当前的采购订单运费,然后遍历 PO 上的项目,跳过已处理的项目(处理积压订单),然后计算应分配给特定行项目的运费

MACRO:GetLineItemFreight
; skip items already billed (handles backorders)
DBMgr.QuerySingleRow(dp_DB, MyDB,
     "select PROCESSED <billed> from 
      PO_RECEIVE where PO_ITEM_ID={rcvCheckList(1, {i})}
      order by RECEIVE_DATE desc, RECEIVE_TIME desc")
ScriptMgr.VerifySelection(dp_verifySelection, GetLineItemFreight, {billed}, 1, "")

; Total = SUM(qty*cost), items = {rcvCheckTotal}
; for each line item: ItemFreight = TotalFreight * (qty*cost)/TotalCost

Math.RPN(dp_RPN, "{totalFreight} 
  {rcvCheckList(7, {i})} {rcvCheckTotal} / * itemFreight STO")

WinMgr.Message(dp_message,
     "Info",
     "totalFreight={totalFreight},
     7,i={rcvCheckList(7, {i})},
     rcvCheckTotal={rcvCheckTotal},
     itemFreight={itemFreight}")

对于列表中的第一个项目

  totalFreight=37.56
  qty*cost=162.88
  purchase order total (rcvCheckTotal) = 1,987.03

根据计算,这应该为该项目分配 3.08 美元的运费。相反,我们得到了 6117.77 美元的运费,就像我们的小调试信息所示!这就像 PO 总额 = 1 美元,而我们只是将运费和项目成本相乘!(线索!)下一站,RPN 函数。

RPN 函数

RPN 函数非常简单,不值得评论。所有运算符都派生自基类,并且执行操作的方法 Go 是一个虚方法。

void RPN::Go(void)
{
    while (rpn[0])
    {
        AutoString token=GetToken(rpn);
        RPNOperator* oper=FindOperator(token);
        if (oper)
        {
            AutoString result;
            switch(oper->GetNumParams())
            {
                case 1:
                {
                    AutoString v=valueStack.top();
                    valueStack.pop();
                    result=oper->Go(v);
                    break;
                }
                case 2:
                {
                    AutoString v1=valueStack.top();
                    valueStack.pop();
                    AutoString v2=valueStack.top();
                    valueStack.pop();
                    result=oper->Go(v1, v2);
                    break;
                }
            }

            if (result != "")
            {
                valueStack.push(result);
            }
        }
        else
        {
            valueStack.push(token);
        }
    }
}

现在让我们看一个特定的函数,在这种情况下是除法函数

AutoString RPNOperatorDivide::Go(const AutoString& v1, const AutoString& v2)
{
    return AutoString(atof(v2) / atof(v1));
}

哇。还有什么比这更简单的呢?追踪进去,我们发现

v1="1,987.03"
v2="162.88"

但是等等!!!

BUG!!!

atof 函数太简单了!给定“1,987.03”,它返回 1.0000!!!

为什么我们以前没有看到?嗯,首先,这段代码是在发票金额小于 1000 美元的情况下进行测试的。其次,后来我修改了脚本,使其以逗号格式化数字。第三,船厂收到的超过 1000 美元的发票并不多,因此这个问题发生在我们身上已经有一段时间了。

修复方案

信不信由你,我以前就遇到过完全相同的 Bug,但没有正确修复,而是选择了针对手头问题的一个不同的解决方案,从而再次被同一个 Bug 咬到。通用的解决方案是在 RPN 代码中通过删除逗号来修复它。当然,这也需要改变方法的签名,因为它们以前是 const 引用

AutoString RPNOperatorDivide::Go(AutoString v1, AutoString v2)
{
    v1.Replace(",", "");
    v2.Replace(",", "");
    return AutoString(atof(v2) / atof(v1));
}

结论

这一切以及 AAL 技术的美妙之处在于,现在这个问题在所有我用 AAL 编写的应用程序中都得到了永久修复。而且,因为 RPN 函数在 mathAtmtn.dll 中,我只需要更新 DLL,而不需要重新编译我用这项技术编写的每一个应用程序。

现在,这段代码中还有另一个 Bug。想猜猜是什么吗?

Postscript

我女朋友给我添了很多麻烦。我告诉她我没有拼写检查这篇文章,她说:“Marc Clifton!你怎么敢!你对别人拼写检查的事情如此苛刻!”当然,她立刻发现了一个拼写错误!“你得不到 5 分”,她说。

© . All rights reserved.