在 JavaScript 中,将 .NET 中的字符串与序号忽略大小写进行比较的等效物是什么?

What is the equivalent in JavaScript of comparing strings in .NET with ordinal ignore case?

提问人:rory.ap 提问时间:10/21/2023 更新时间:10/23/2023 访问量:88

问:

在 .NET 中,我们能够使用序号比较来比较字符串,同时忽略大小写这是字符串比较的最佳实践,尤其是当可能涉及多种区域性时。

我正在寻找 JavaScript 中的完全等效项。关于 JS 字符串比较的答案很多,但我找不到太多关于序数比较的答案,更不用说这样做了并忽略了大小写。我确实发现了这个关于在不忽略大小写的情况下以序数方式比较字符串的问题,但不清楚在忽略大小写的情况下我将如何做到这一点,所以我认为这是一个根本不同的问题(因此我在这里提出了新问题)。

如何在 JavaScript 中以有序方式比较字符串,同时忽略大小写,就像在 .NET 中一样?

javascript .net 字符串比较 大小写

评论

1赞 T.J. Crowder 10/21/2023
序数忽略大小写是一个有趣的概念,尤其适用于来自多种区域性的字符串,因为“小写”和“大写”字符的概念本质上是特定于区域性的。我以前从未真正想过Microsoft的“序数忽略案例”的真正含义(尽管我已经使用过它,所以我可能应该使用)。嗯.....
0赞 T.J. Crowder 10/21/2023
我怀疑这里的答案是在两边都使用(或),然后使用或如您链接到的答案中所述。这些方法使用不区分 Unicode 区域设置的大小写映射进行转换。toLowerCasetoUpperCase<>
0赞 zabuli 10/21/2023
根据这个答案:stackoverflow.com/a/51005583/12319387 string::localeCompare 支持不区分大小写的比较(以及其他强大的功能)。
0赞 T.J. Crowder 10/21/2023
@zabuli - 确实如此,但它不是序数。它是区域设置感知的。我试图找出是否有可以传递的“区域设置”标识符,该标识符(基本上)表示“不要感知区域设置”。

答:

0赞 dragon 10/21/2023 #1

在 javascript 中有两种方法可以实现这一点:

  1. 将字符串与 and 运算符进行比较(它们执行序号比较),并在使用 或 之前不区分大小写。<>.toLowerCase.toUpperCase
  2. 编写一个自定义方法,该方法将 ASCII 值作为字符串中每个字符的数字进行获取,并手动进行比较。您可以使用该方法在 javascript 中实现此目的。这将返回 UTF-16 字符集的索引,但前 128 个代码点在 UTF-16 和 ASCII 之间共享,这应该不是问题,除非您需要扩展 ASCII...string.charCodeAt(index)

这可以这样实现:

function compareStringOrdinalIgnoreCase(str1, str2) {
  str1 = str1.toUpperCase()
  str2 = str2.toUpperCase()
  for (let i = 0; i < Math.min(str1.length, str2.length); i++) {
    const charCode1 = str1.charCodeAt(i);
    const charCode2 = str2.charCodeAt(i);

    if (charCode1 < charCode2) {
      return -1;
    } else if (charCode1 > charCode2) {
      return 1;
    }
  }

  if (str1.length < str2.length) {
    return -1;
  } else if (str1.length > str2.length) {
    return 1;
  }

  return 0;
}
console.log(compareStringOrdinalIgnoreCase("Id", "id"))

评论

1赞 T.J. Crowder 10/21/2023
IIRC,对于某些字符来说是一个有损操作。对于上述情况,可能是稍微好一点的选择。toUpperCasetoLowerCase
0赞 Blindy 10/21/2023
@T.J.Crowder:这说起来很奇怪,小写和大写变换都是有损的——它们是单向的、不可逆的函数。
0赞 T.J. Crowder 10/21/2023
@Blindy - 好吧,那就更有损耗了。例如,丢失变音符号。(我不得不说,我发现“愚蠢”是不必要的,而且没有建设性。
0赞 rory.ap 10/24/2023
@Blindy同意T.J.克劳德(T.J. Crowder)关于“这是一件愚蠢的事情”的观点。也许你应该这样说,“有趣的是,你认为,你能详细说明一下吗?因为我的理解是......”
0赞 Blindy 10/24/2023
这既不有趣,我也不需要详细说明,他确切地知道我的意思。
0赞 T.J. Crowder 10/23/2023 #2

很难证明是否定的(人们总是可以忽略某些东西),但据我所知,标准运行时中没有内置直接的等效操作。正如您在问题中指出的那样,直接序比较由 和 完成。其他比较是通过 完成的,但是虽然它有忽略大小写的选项,但它适用于特定于区域设置的规则,你已经说过你不想要这些规则。(我确实想知道是否有“ordinal”的区域设置说明符,但如果有,我还没有找到它。<>localeCompare

鉴于此,我们最接近的方法是将两边转换为相同的大小写,并将结果与 和 进行比较。/ 操作使用不区分区域设置的大小写映射<>toLowerCasetoUpperCase

必须根据 Unicode 字符数据库中不区分区域设置的大小写映射派生结果(这不仅显式包括文件 UnicodeData.txt,还包括随附的文件 SpecialCasing.txt 中的所有不区分区域设置的映射)。

所以沿着这些思路:

function ordinalCompareInsensitive(a, b) {
    const lowera = a.toLowerCase();
    const lowerb = b.toLowerCase();
    if (lowera === lowerb) {
        return 0;
    }
    if (lowera < lowerb) {
        return -1;
    }
    return 1;
}

虽然不会成为过早优化的牺牲品,但我会注意到,在性能特别受关注的极少数情况下,上述内容略微偏向于假设字符串匹配(从那时起,它只是匹配,但不是那么好)。如果在边缘情况下,性能至关重要,则可以选择比较顺序以适合要比较的数据。===<

无论比较的顺序如何,上述内容(当然)必须在开始之前完全转换两个字符串。除非被比较的字符串特别长,否则我不指望你能从避免这种情况中获得任何好处(有利于自己做循环,在你进行时转换每个字符,并在找到答案后立即短路)。但是,如果您找到它的用例,那将是一个选项。您必须决定是比较代码单元还是执行比较代码的稍微复杂的事情。我可能会选择代码点,因为它们更有意义,但这取决于您的用例。and 运算符在代码单元级别工作,但他们不必担心正确的大小写映射。<>

就其价值而言,至少有两种方法可以通过代码点来实现:使用迭代器和使用 .下面是一个使用迭代器(TypeScript,但注释掉了类型注释)的示例:codePointAt

// Again, I doubt you'd need to do your own loop for this, but just in case:
function ordinalCompareInsensitive2(a/*: string*/, b/*: string*/)/*: number */ {
    const itA = a[Symbol.iterator]();
    const itB = b[Symbol.iterator]();
    let rA/*: IteratorResult<string, any>*/;
    let rB/*: IteratorResult<string, any>*/;
    while (true) {
        rA = itA.next();
        rB = itB.next();
        if (rA.done) {
            return rB.done ? 0 : -1;
        } else if (rB.done) {
            return 1;
        }
        const chA = rA.value.toLowerCase();
        const chB = rB.value.toLowerCase();
        if (chA < chB) {
            return -1;
        }
        if (chA > chB) {
            return 1;
        }
    }
}

或者 with(请注意,您传递的索引是以代码单元表示的索引,这就是为什么代码会按找到的字符的长度 [可能是多个代码单元] 移动索引的原因):codePointAt

function ordinalCompareInsensitive3(a/*: string*/, b/*: string*/)/*: number */ {
    let indexA = 0;
    let indexB = 0;

    while (true) {
        if (indexA >= a.length) {
            return indexB >= b.length ? 0 : -1;
        } else if (indexB >= b.length) {
            return 1;
        }
        const chA = String.fromCodePoint(a.codePointAt(indexA)/*!*/).toLowerCase();
        const chB = String.fromCodePoint(b.codePointAt(indexB)/*!*/).toLowerCase();
        if (chA < chB) {
            return -1;
        }
        if (chA > chB) {
            return 1;
        }
        indexA += chA.length;
        indexB += chB.length;
    }
}

这些有点即兴发挥,您需要在使用它们之前对其进行审核,尽管我已经使用一些基本输入测试了它们:

function ordinalCompareInsensitive(a/*: string*/, b/*: string*/)/*: number */ {
    const lowera = a.toLowerCase();
    const lowerb = b.toLowerCase();
    if (lowera === lowerb) {
        return 0;
    }
    if (lowera < lowerb) {
        return -1;
    }
    return 1;
}

function ordinalCompareInsensitive2(a/*: string*/, b/*: string*/)/*: number */ {
    const itA = a[Symbol.iterator]();
    const itB = b[Symbol.iterator]();
    let rA/*: IteratorResult<string, any>*/;
    let rB/*: IteratorResult<string, any>*/;
    while (true) {
        rA = itA.next();
        rB = itB.next();
        if (rA.done) {
            return rB.done ? 0 : -1;
        } else if (rB.done) {
            return 1;
        }
        const chA = rA.value.toLowerCase();
        const chB = rB.value.toLowerCase();
        if (chA < chB) {
            return -1;
        }
        if (chA > chB) {
            return 1;
        }
    }
}

function ordinalCompareInsensitive3(a/*: string*/, b/*: string*/)/*: number */ {
    let indexA = 0;
    let indexB = 0;

    while (true) {
        if (indexA >= a.length) {
            return indexB >= b.length ? 0 : -1;
        } else if (indexB >= b.length) {
            return 1;
        }
        const chA = String.fromCodePoint(a.codePointAt(indexA)/*!*/).toLowerCase();
        const chB = String.fromCodePoint(b.codePointAt(indexB)/*!*/).toLowerCase();
        if (chA < chB) {
            return -1;
        }
        if (chA > chB) {
            return 1;
        }
        indexA += chA.length;
        indexB += chB.length;
    }
}

function usLocaleCompareInsensitive(a/*: string*/, b/*: string*/)/*: number */ {
    return a.localeCompare(b, undefined, { sensitivity: "accent" });
}

function clampReturn(x/*: number */)/*: number */ {
    if (x === 0) {
        return x;
    }
    if (x < 0) {
        return -1;
    }
    return 1;
}

function test(a/*: string*/, b/*: string*/, expect/*: number*/) {
    const o1 = clampReturn(ordinalCompareInsensitive(a, b));
    const o2 = clampReturn(ordinalCompareInsensitive2(a, b));
    const o3 = clampReturn(ordinalCompareInsensitive3(a, b));
    const rUS = clampReturn(usLocaleCompareInsensitive(a, b));
    const result = o1 === o2 && o2 === o3 && o3 === rUS && rUS === expect ? "OK" : "<== ERROR !";
    console.log(`${a} vs. ${b}: ${o1} ${o2} ${o3} ${rUS} expect ${expect} ${result}`);
}

test("abc", "abc", 0);
test("abc", "ABC", 0);
test("abc", "aBC", 0);
test("abc", "abcd", -1);
test("abcd", "abc", 1);
test("abc", "acd", -1);
test("acd", "abc", 1);
.as-console-wrapper {
    max-height: 100% !important;
}


我在上面使用了(而不是),因为我发现字符在从小写字母转换为大写字母然后再转换回来后,与开始时不同;但我没有发现相反的情况(从大写字母转换为小写字母,然后再转换回来)。这是一个边缘情况,但你必须选择一个或另一个,所以我选择了看起来更安全的那个。以下是我的检查方式:toLowerCasetoUpperCase

// Tell the in-snippet console not to throw away old log entries
console.config({
    maxEntries: Infinity
});

const MAX_UNICODE = 0x10ffff;

function ucode(ch/*: string*/) {
    return `\\u${ch
        .codePointAt(0)/*!*/
        .toString(16)
        .toUpperCase()
        .padStart(4, "0")}`;
}

function check(
    type/*: string*/,
    convertToMethod/*: "toLowerCase" | "toUpperCase"*/,
    convertBackMethod/*: "toLowerCase" | "toUpperCase"*/
) {
    console.log(
        `Checking '${type}' characters converted with ${convertToMethod} then back with ${convertBackMethod}:`
    );
    let found = 0;
    let mismatches = 0;
    for (let n = 1; n < MAX_UNICODE; ++n) {
        const char = String.fromCodePoint(n);
        const converted = char[convertToMethod]();
        if (char !== converted) {
            ++found;
            if (
                char.localeCompare(converted, "en-US", {
                    sensitivity: "accent",
                }) !== 0
            ) {
                ++mismatches;
                const reconverted = converted[convertBackMethod]();
                console.log(
                    `${char} vs ${converted} vs ${reconverted} (${ucode(
                        char
                    )} vs. ${ucode(converted)} vs. ${ucode(reconverted)})`
                );
            }
        }
    }
    console.log(
        `Done, ${mismatches} mis-matches found (total "${type}" chars found: ${found})`
    );
}

check("upper case", "toLowerCase", "toUpperCase");
check("lower case", "toUpperCase", "toLowerCase");
.as-console-wrapper {
    max-height: 100% !important;
}

不过,这只会单独检查每个代码点。在某些语言中,代码点的组合可能是有意义的,但我没有尝试在上面的简单测试中允许这样做。(但我确实让它在代码级别工作,而不仅仅是代码单元级别。