Skip to content

Instantly share code, notes, and snippets.

@azjezz
Created May 14, 2025 20:11
Show Gist options
  • Save azjezz/6586d4695aa0e6944c07e1644da71c7e to your computer and use it in GitHub Desktop.
Save azjezz/6586d4695aa0e6944c07e1644da71c7e to your computer and use it in GitHub Desktop.
PHP's string increment/decrement logic, in Rust.
/// Increments an alphanumeric string.
///
/// Rust implementation based on PHP's str_increment function from php-src:
/// https://github.com/php/php-src/blob/1de16c7f15f3f927bf7e7c26b3a6b1bd5803b1cc/ext/standard/string.c#L1227
///
/// # Arguments
///
/// * `input` - The string to increment
///
/// # Returns
///
/// * `Some(String)` - The incremented string on success
/// * `None` - If the input is empty or contains non-alphanumeric ASCII characters
pub fn str_increment(input: &str) -> Option<String> {
// Check if string is empty
if input.is_empty() {
return None;
}
// Check if string only contains alphanumeric ASCII characters
if !input.chars().all(|c| c.is_ascii_alphanumeric()) {
return None;
}
let mut chars: Vec<char> = input.chars().collect();
let mut position = chars.len() - 1;
let mut carry = true;
while carry && position < chars.len() {
let c = chars[position];
match c {
'a'..='y' | 'A'..='Y' | '0'..='8' => {
chars[position] = (c as u8 + 1) as char;
carry = false;
}
'z' => {
chars[position] = 'a';
if position == 0 {
chars.insert(0, 'a');
carry = false;
}
}
'Z' => {
chars[position] = 'A';
if position == 0 {
chars.insert(0, 'A');
carry = false;
}
}
'9' => {
chars[position] = '0';
if position == 0 {
chars.insert(0, '1');
carry = false;
}
}
_ => unreachable!("We already checked that all characters are alphanumeric"),
}
if carry && position > 0 {
position -= 1;
} else {
break;
}
}
Some(chars.into_iter().collect())
}
/// Decrements an alphanumeric string.
///
/// Rust implementation based on PHP's str_decrement function from php-src:
/// https://github.com/php/php-src/blob/1de16c7f15f3f927bf7e7c26b3a6b1bd5803b1cc/ext/standard/string.c#L1283
///
/// # Arguments
///
/// * `input` - The string to decrement
///
/// # Returns
///
/// * `Some(String)` - The decremented string on success
/// * `None` - If the input is empty, contains non-alphanumeric ASCII characters,
/// or is out of decrement range (like "0" or "a")
pub fn str_decrement(input: &str) -> Option<String> {
// Check if string is empty
if input.is_empty() {
return None;
}
// Check if string only contains alphanumeric ASCII characters
if !input.chars().all(|c| c.is_ascii_alphanumeric()) {
return None;
}
// Check if first character is '0' (cannot be decremented)
if input.starts_with('0') && input.len() > 1 {
return None;
}
let mut chars: Vec<char> = input.chars().collect();
let mut position = chars.len() - 1;
let mut carry = true;
while carry && position < chars.len() {
let c = chars[position];
match c {
'b'..='z' | 'B'..='Z' | '1'..='9' => {
chars[position] = (c as u8 - 1) as char;
carry = false;
}
'a' => {
chars[position] = 'z';
if position == 0 {
// If the string is just "a", it can't be decremented
if chars.len() == 1 {
return None;
}
// Otherwise, remove the first character and continue
chars.remove(0);
carry = false;
}
}
'A' => {
chars[position] = 'Z';
if position == 0 {
// If the string is just "A", it can't be decremented
if chars.len() == 1 {
return None;
}
// Otherwise, remove the first character and continue
chars.remove(0);
carry = false;
}
}
'0' => {
chars[position] = '9';
if position == 0 {
// If the string is just "0", it can't be decremented
if chars.len() == 1 {
return None;
}
// Otherwise, remove the first character and continue
chars.remove(0);
carry = false;
}
}
_ => unreachable!("We already checked that all characters are alphanumeric"),
}
if carry && position > 0 {
position -= 1;
} else {
break;
}
}
// Remove leading 0 if the string has more than one character
if chars.len() > 1 && chars[0] == '0' {
chars.remove(0);
}
Some(chars.into_iter().collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_increment_basic() {
assert_eq!(str_increment("hello"), Some("hellp".to_string()));
assert_eq!(str_increment("PHP"), Some("PHQ".to_string()));
assert_eq!(str_increment("rust"), Some("rusu".to_string()));
assert_eq!(str_increment("abc123"), Some("abc124".to_string()));
}
#[test]
fn test_increment_with_carries() {
assert_eq!(str_increment("hellz"), Some("helma".to_string()));
assert_eq!(str_increment("TESTZ"), Some("TESUA".to_string()));
assert_eq!(str_increment("xyz"), Some("xza".to_string()));
assert_eq!(str_increment("zz"), Some("aaa".to_string()));
assert_eq!(str_increment("ZZ"), Some("AAA".to_string()));
}
#[test]
fn test_increment_with_numeric_carries() {
assert_eq!(str_increment("9"), Some("10".to_string()));
assert_eq!(str_increment("99"), Some("100".to_string()));
assert_eq!(str_increment("999"), Some("1000".to_string()));
assert_eq!(str_increment("abc9"), Some("abd0".to_string()));
assert_eq!(str_increment("abc99"), Some("abd00".to_string()));
}
#[test]
fn test_increment_mixed_alphanumeric() {
assert_eq!(str_increment("a9"), Some("b0".to_string()));
assert_eq!(str_increment("a99z"), Some("b00a".to_string()));
assert_eq!(str_increment("Z9"), Some("AA0".to_string()));
assert_eq!(str_increment("9z"), Some("10a".to_string()));
assert_eq!(str_increment("9Z"), Some("10A".to_string()));
}
#[test]
fn test_increment_at_boundaries() {
assert_eq!(str_increment("z"), Some("aa".to_string()));
assert_eq!(str_increment("Z"), Some("AA".to_string()));
}
#[test]
fn test_increment_failure_cases() {
assert_eq!(str_increment(""), None);
assert_eq!(str_increment("hello!"), None);
assert_eq!(str_increment("test-123"), None);
assert_eq!(str_increment("[email protected]"), None);
assert_eq!(str_increment("русский"), None); // Non-ASCII characters
}
#[test]
fn test_decrement_basic() {
assert_eq!(str_decrement("hellp"), Some("hello".to_string()));
assert_eq!(str_decrement("PHQ"), Some("PHP".to_string()));
assert_eq!(str_decrement("rusu"), Some("rust".to_string()));
assert_eq!(str_decrement("abc124"), Some("abc123".to_string()));
}
#[test]
fn test_decrement_with_carries() {
assert_eq!(str_decrement("helma"), Some("hellz".to_string()));
assert_eq!(str_decrement("TESTAA"), Some("TESSZZ".to_string()));
assert_eq!(str_decrement("xza"), Some("xyz".to_string()));
assert_eq!(str_decrement("aaa"), Some("zz".to_string()));
assert_eq!(str_decrement("AAA"), Some("ZZ".to_string()));
}
#[test]
fn test_decrement_with_numeric_carries() {
assert_eq!(str_decrement("10"), Some("9".to_string()));
assert_eq!(str_decrement("100"), Some("99".to_string()));
assert_eq!(str_decrement("1000"), Some("999".to_string()));
assert_eq!(str_decrement("abc10"), Some("abc09".to_string()));
assert_eq!(str_decrement("abc100"), Some("abc099".to_string()));
}
#[test]
fn test_decrement_mixed_alphanumeric() {
assert_eq!(str_decrement("b0"), Some("a9".to_string()));
assert_eq!(str_decrement("b00a"), Some("a99z".to_string()));
assert_eq!(str_decrement("AA0"), Some("Z9".to_string()));
assert_eq!(str_decrement("10a"), Some("9z".to_string()));
assert_eq!(str_decrement("10A"), Some("9Z".to_string()));
}
#[test]
fn test_decrement_at_boundaries() {
assert_eq!(str_decrement("aa"), Some("z".to_string()));
assert_eq!(str_decrement("a"), None);
assert_eq!(str_decrement("AA"), Some("Z".to_string()));
assert_eq!(str_decrement("A"), None);
}
#[test]
fn test_decrement_leading_zeros() {
assert_eq!(str_decrement("01"), None);
assert_eq!(str_decrement("001"), None);
assert_eq!(str_decrement("0abc"), None);
}
#[test]
fn test_decrement_failure_cases() {
assert_eq!(str_decrement(""), None);
assert_eq!(str_decrement("0"), None);
assert_eq!(str_decrement("hello!"), None);
assert_eq!(str_decrement("test-123"), None);
assert_eq!(str_decrement("[email protected]"), None);
assert_eq!(str_decrement("русский"), None);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment