如何解析和捕获任何测量单位

How to parse and capture any measurement unit

本文关键字:任何 测量 单位 何解析      更新时间:2023-09-26

在我的应用程序中,用户可以自定义测量单位,因此如果他们想以分米而不是英寸工作,或者以整圈而不是度工作,他们可以。但是,我需要一种方法来解析包含多个值和单元的字符串,例如1' 2" 3/8。我在SO上看到了一些正则表达式,但没有发现任何一个与帝国系统的所有情况相匹配,更不用说允许任何类型的单位了。我的目标是拥有尽可能宽松的输入框。

所以我的问题是:如何以最方便用户的方式从字符串中提取多个值单位对


我想出了以下算法:

  1. 检查是否存在非法字符,并在需要时抛出错误
  2. 修剪前导空格和尾部空格
  3. 每次出现非数字字符后跟数字字符时,将字符串拆分为多个部分,除了.、/其用于识别小数和分数
  4. 删除部件中的所有空格,检查字符是否误用(多个小数点或分数条),并将''替换为"
  5. 拆分每个部分的值和单位字符串。如果零件没有单元:
    • 如果是第一部分,请使用默认单位
    • 否则,如果它是一个分数,则将其视为与前一部分相同的单元
    • 否则,如果不是,则根据上一个零件的单位将其视为英寸、厘米或毫米
    • 如果它不是第一部分,并且无法猜测单元,则抛出一个错误
  6. 检查单位是否有意义,是否都是相同的系统(公制/英制),并遵循降序(ft>in>分数或m>cm>mm>分数),如果没有,则抛出错误
  7. 对所有部分进行转换和求和,在此过程中进行除法运算

我想我可以使用字符串操作函数来完成大部分工作,但我觉得必须有一种更简单的方法来使用regex。


我想出了一个正则表达式:
(('d+('|''|"|m|cm|mm|'s|$) *)+('d+('/'d+)?('|''|"|m|cm|mm|'s|$) *)?)|(('d+('|''|"|m|cm|mm|'s) *)*('d+('/'d+)?('|''|"|m|cm|mm|'s|$) *))

它只允许在末尾使用分数,并允许在值之间放置空格。不过,我从未使用过regex捕获,所以我不太确定如何从这种混乱中提取值。我明天再做这件事。

我的目标是拥有尽可能宽松的输入框。

谨慎、宽容并不总是意味着更直观。模糊的输入应该警告用户,而不是无声地传递,因为这可能会导致他们在意识到自己的输入没有像他们希望的那样被解释之前犯下多个错误。

如何从字符串中提取多个值单位对?我想我可以使用字符串操作函数来完成大部分工作,但我觉得必须有一种更简单的方法来使用regex。

正则表达式是一个强大的工具,尤其是因为它们可以在许多编程语言中使用,但请注意。当你拿着锤子时,一切都开始看起来像钉子。不要因为你最近学习了正则表达式的工作原理就试图用正则表达式来解决每一个问题。

查看您编写的伪代码,您试图同时解决两个问题:拆分字符串(我们称之为标记化)和根据语法解释输入(我们称其为解析)。您应该首先尝试将输入拆分为一个令牌列表,或者可能是单位值对。一旦完成了字符串操作,就可以开始理解这些对了。关注点的分离将使您不必头疼,因此您的代码将更容易维护。

不过,我从未使用过regex捕获,所以我不太确定如何从这种混乱中提取值。

如果正则表达式具有全局(g)标志,则可以使用它在同一字符串中查找多个匹配项。如果您有一个正则表达式来查找单个单位值对,那么这将非常有用。在JavaScript中,可以使用string.match(regex)检索匹配项列表。但是,该函数会忽略全局正则表达式上的捕获组。

如果要使用捕获组,则需要在循环中调用regex.exec(string)。对于每个成功的匹配,exec函数将返回一个数组,其中项目0是整个匹配,项目1及以后是捕获的组。

例如,/('d+) ([a-z]+)/g将查找一个后跟空格和单词的整数。如果你连续拨打regex.exec("1 hour 30 minutes"),你会得到:

  • ["1 hour", "1", "hour"]
  • ["30 minutes", "30", "minutes"]
  • null

连续调用的工作方式是这样的,因为regex对象保留了一个内部游标,您可以使用regex.lastIndex获取或设置该游标。在使用不同的输入再次使用regex之前,您应该将其设置回0。

您一直在使用括号来隔离OR子句(如a|b),并将量词应用于字符序列(如(abc)+)。如果要在不创建捕获组的情况下执行此操作,可以使用(?: )。这被称为非捕获组。它的作用与正则表达式中的正则括号相同,但它内部的内容不会在返回的数组中创建条目。

有没有更好的方法来解决这个问题?

这个答案的前一个版本以一个正则表达式结尾,这个正则表达式比问题中发布的更令人费解,因为当时我还不清楚,但今天这将是我的建议。它是一个正则表达式,每次只从输入字符串中提取一个令牌。

/ ('s+)                             // 1 whitespace
| ('d+)'/('d+)                      // 2,3 fraction
| ('d*)([.,])('d+)                  // 4,5,6 decimal
| ('d+)                             // 7 integer
| (km|cm|mm|m|ft|in|pi|po|'|")      // 8 unit
/gi

很抱歉突出显示了奇怪的语法。我使用了空白使其可读性更强,但格式正确,它变成了:

/('s+)|('d+)'/('d+)|('d*)([.,])('d+)|('d+)|(km|cm|mm|m|ft|in|pi|po|'|")/gi

这个正则表达式巧妙地利用了由OR子句分隔的捕获组。只有一种类型的令牌的捕获组才会包含任何内容。例如,在字符串"10 ft"上,对exec的连续调用将返回:

  • ["10", "", "", "", "", "", "", "10", ""](因为"10"是整数)
  • [" ", " ", "", "", "", "", "", "", ""](因为"是空白)
  • ["ft", "", "", "", "", "", "", "", "ft"](因为"英尺"是一个单位)
  • null

令牌化器函数可以这样处理每个单独的令牌:

function tokenize (input) {
    const localTokenRx = new RegExp(tokenRx);
    return function next () {
        const startIndex = localTokenRx.lastIndex;
        if (startIndex >= input.length) {
            // end of input reached
            return undefined;
        }
        const match = localTokenRx.exec(input);
        if (!match) {
            localTokenRx.lastIndex = input.length;
            // there is leftover garbage at the end of the input
            return ["garbage", input.slice(startIndex)];
        }
        if (match.index !== startIndex) {
            localTokenRx.lastIndex = match.index;
            // the regex skipped over some garbage
            return ["garbage", input.slice(startIndex, match.index)];
        }
        const [
            text,
            whitespace,
            numerator, denominator,
            integralPart, decimalSeparator, fractionalPart,
            integer,
            unit
        ] = match;
        if (whitespace) {
            return ["whitespace", undefined];
            // or return next(); if we want to ignore it
        }
        if (denominator) {
            return ["fraction", Number(numerator) / Number(denominator)];
        }
        if (decimalSeparator) {
            return ["decimal", Number(integralPart + "." + fractionalPart)];
        }
        if (integer) {
            return ["integer", Number(integer)];
        }
        if (unit) {
            return ["unit", unit];
        }
    };
}

这个函数可以在一个地方完成所有必要的字符串操作和类型转换,让另一段代码对令牌序列进行正确的分析。但这将超出这个Stack Overflow答案的范围,特别是因为这个问题没有指定我们愿意接受的语法规则。

但是,如果你只想接受英制长度和公制长度,那么这很可能是一个过于通用和复杂的解决方案。为此,我可能只会为每个可接受的格式编写一个不同的正则表达式,然后测试用户的输入,看看哪一个匹配。如果两个不同的表达式匹配,那么输入是不明确的,我们应该警告用户。