使用 preg_replace() 将由驼峰式命名法表示的字母数字字符串转换为下划线式命名法。

13

我现在有一个方法,可以将我的驼峰字符串转换为蛇形命名法,但它被分成了三个调用 preg_replace() 的步骤:

public function camelToUnderscore($string, $us = "-")
{
    // insert hyphen between any letter and the beginning of a numeric chain
    $string = preg_replace('/([a-z]+)([0-9]+)/i', '$1'.$us.'$2', $string);
    // insert hyphen between any lower-to-upper-case letter chain
    $string = preg_replace('/([a-z]+)([A-Z]+)/', '$1'.$us.'$2', $string);
    // insert hyphen between the end of a numeric chain and the beginning of an alpha chain
    $string = preg_replace('/([0-9]+)([a-z]+)/i', '$1'.$us.'$2', $string);

    // Lowercase
    $string = strtolower($string);

    return $string;
}

我编写了测试来验证其准确性,并且它可以正确地处理以下输入数组(array('input' => 'output')):

$test_values = [
    'foo'       => 'foo',
    'fooBar'    => 'foo-bar',
    'foo123'    => 'foo-123',
    '123Foo'    => '123-foo',
    'fooBar123' => 'foo-bar-123',
    'foo123Bar' => 'foo-123-bar',
    '123FooBar' => '123-foo-bar',
];

我在想是否有一种方法可以将我的 preg_replace() 调用减少到一行,从而获得相同的结果。有什么想法吗?

注意:参考这篇文章,我的研究已经给出了一个 preg_replace() 正则表达式,它可以让我获得几乎想要的结果,但它不能将 foo123 转换为 foo-123


1
@AdrienLeber请阅读我问题的底部。这不是重复的。我已经阅读了那篇文章,但它并没有解决我的问题。 - Matt
抱歉,我已删除了重复标记,并根据您在问题中提到的帖子发布了一个新答案。 - JazZ
@pete 注意了 - Matt
@Matt 是的,我有点急功近利了,抱歉。 - Pieter van den Ham
研究人员常常寻找“snake_case”,但您的[mcve]正在寻找“kebab-case”...这些字符串正在被“串起来”。 - mickmackusa
4个回答

28
你可以使用“零宽断言”在一个正则表达式中完成所有这些操作:
function camelToUnderscore($string, $us = "-") {
    return strtolower(preg_replace(
        '/(?<=\d)(?=[A-Za-z])|(?<=[A-Za-z])(?=\d)|(?<=[a-z])(?=[A-Z])/', $us, $string));
}

正则表达式演示

代码演示

正则表达式描述:

(?<=\d)(?=[A-Za-z])  # if previous position has a digit and next has a letter
|                    # OR
(?<=[A-Za-z])(?=\d)  # if previous position has a letter and next has a digit
|                    # OR
(?<=[a-z])(?=[A-Z])  # if previous position has a lowercase and next has a uppercase letter

1
非常好的答案和解释。这正是我所需要的。谢谢。 - Matt
很好的解决方案,但在使用 preg_replace 时要注意代码注入。上述解决方案将会引入代码注入漏洞。 - Sanket Gandhi

4

根据我之前标记的重复帖子,我想分享一些个人见解。这里的已被采纳的解决方案非常棒。我只是想尝试用之前分享的内容来解决它:

function camelToUnderscore($string, $us = "-") {
    return strtolower(preg_replace('/(?<!^)[A-Z]+|(?<!^|\d)[\d]+/', $us.'$0', $string));
}

例子:

Array
(
    [0] => foo
    [1] => fooBar
    [2] => foo123
    [3] => 123Foo
    [4] => fooBar123
    [5] => foo123Bar
    [6] => 123FooBar
)

foreach ($arr as $item) {
    echo camelToUnderscore($item);
    echo "\r\n";
}

输出:

foo
foo-bar
foo-123
123-foo
foo-bar-123
foo-123-bar
123-foo-bar
说明:
(?<!^)[A-Z]+      // Match one or more Capital letter not at start of the string
|                 // OR
(?<!^|\d)[\d]+    // Match one or more digit not at start of the string

$us.'$0'          // Substitute the matching pattern(s)

在线正则表达式

这个问题已经得到了解决,所以我不会说希望它有所帮助,但也许有人会发现这个工具有用。


编辑

这个正则表达式有一些限制:

foo123bar => foo-123bar
fooBARFoo => foo-barfoo

感谢@urban指出。这里是他发布的带有三个解决方案测试的链接:

三个解决方案演示


你的解决方案与 OP 的解决方案不同:它没有考虑到 foo123bar 这种情况... 请参见 代码演示,比较 OP 的解决方案、anubhava 的解决方案和你的解决方案。 - Urban
@urban foo123bar 不是驼峰命名法。但你说得对,这个正则表达式有限制,不是最好的解决方案...像 fooBARFoo 这样的字符串会产生 foo-barfoo 的结果。无论如何,这将适用于基本的驼峰命名法。我已经编辑了答案。感谢您的反馈! - JazZ

2

一位同事提供了以下代码:$string = preg_replace(array($pattern1, $pattern2), $us.'$1', $string); 可能会起作用。

我的解决方案:

public function camelToUnderscore($string, $us = "-")
{
    $patterns = [
        '/([a-z]+)([0-9]+)/i',
        '/([a-z]+)([A-Z]+)/',
        '/([0-9]+)([a-z]+)/i'
    ];
    $string = preg_replace($patterns, '$1'.$us.'$2', $string);

    // Lowercase
    $string = strtolower($string);

    return $string;
}

0

你不需要忍受大量的回溯或多个模式集来定位单词或连续数字之间的位置的低效率。

使用贪婪匹配来查找所需的序列,然后使用\K重置完整字符串匹配,然后检查该位置不是字符串的结尾。所有符合条件的内容都应该接收分隔字符。这种贪婪模式的速度在于它消耗一个或多个序列,并且永远不会回头查看。

我将从我的答案中省略strtolower()调用,因为它只是挑战中的噪音。

代码:(演示

preg_replace(
    '/(?:\d++|[A-Za-z]?[a-z]++)\K(?!$)/',
    '-',
    $tests
)

单词/数字之间的处理:

用户 步骤 模式 替换
Anubhava 660 /(?<=\d)(?=[A-Za-z])|(?<=[A-Za-z])(?=\d)|(?<=[a-z])(?=[A-Z]) '-'
mickmackusa 337 /(?:\d++|[A-Za-z]?[a-z]++)\K(?!$)/ '-'

严格的驼峰命名法处理:

用户 步骤 模式 替换
JazZ 321 /(?<!^)[A-Z]+|(?<!^|\d)[\d]+/ '-$0'
mickmackusa 250 /(?>\d+|[A-Z][a-z]*|[a-z]+)(?!$)/ '$0-'
mickmackusa 244 /(?:\d++|[a-z]++)\K(?!$)/ '-'
我不认同@Matt的答案,因为它对每个字符串都要进行三次完整的遍历 -- 在效率方面,它甚至不在同一个级别上。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接