Использование preg_replace() для преобразования CamelCase в snake_case

Теперь у меня есть метод, который преобразует мои строки с верблюжьим корпусом в случай змеи, но он разбит на три вызова 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

Вы можете использовать lookarounds для выполнения всего этого в одном регулярном выражении:

function camelToUnderscore($string, $us = "-") {
    return strtolower(preg_replace(
        '/(?<=\d)(?=[A-Za-z])|(?<=[A-Za-z])(?=\d)|(?<=[a-z])(?=[A-Z])/', $us, $string));
}

Демо-версия RegEx

Демо-версия кода

RegEx Описание:

(?<=\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

Ответ 2

Вот мои два цента на основе дублированного сообщения, отмеченного мной ранее. Принятое решение здесь потрясающе. Я просто хотел попытаться решить его с помощью того, что было общим:

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 за это. Вот его ссылка на тесты с тремя решениями, размещенными по этому вопросу:

три демонстрации решений

Ответ 3

От коллеги:

$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;
}