Skip to content

Case Converter — Ruby source

Convert text between UPPER, lower, Title, Sentence, camelCase, PascalCase, snake_case, kebab-case, CONSTANT_CASE and slug. Live, one-click copy.

This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.

# frozen_string_literal: true

# case-converter — pure, deterministic text-case transformations.
#
# Language: Ruby (3.2, standard library only)
# Source:   CosmoDev polyglot showcase port of the Case Converter tool.
#           Canonical: src/lib/case.ts (the live TypeScript) plus this
#           tool's python.py / rust.rs reference ports.
# License:  display source — part of CosmoDev's polyglot tool pages.
#
# Split any input into word tokens and re-emit it in upper / lower / title /
# sentence / camel / pascal / snake / kebab / constant / slug case — same
# inputs produce the same outputs as the TypeScript original, including edge
# cases. Ruby's upcase / downcase are full-Unicode, matching JavaScript's
# toUpperCase / toLowerCase.

module CaseConverter
  # A camelCase seam: a lowercase letter or digit immediately followed by an
  # uppercase letter. We splice a space in here before splitting.
  CAMEL_BOUNDARY = /([a-z0-9])([A-Z])/

  # Word delimiters: any run of whitespace (\p{Space} is the Unicode
  # whitespace property, like the Python and Rust ports), underscore, hyphen,
  # or dot.
  SEPARATORS = /[\p{Space}_\-.]+/

  # Anything that is not an ASCII letter or digit — stripped from each token.
  NON_ALNUM = /[^a-zA-Z0-9]/

  # A title "word": a maximal run that starts with an ASCII word character
  # (Ruby's \w is ASCII-only by default, exactly like JavaScript's) and
  # continues with non-whitespace.
  TITLE_WORD = /\w\S*/

  module_function

  # Tokenize +s+ into word units.
  #
  # The pipeline reproduces the TypeScript reference step for step:
  #   1. Insert a space at every camelCase boundary.
  #   2. Split on runs of whitespace / underscore / hyphen / dot.
  #   3. Strip remaining non-alphanumerics and drop empty tokens.
  def words(s)
    spaced = s.gsub(CAMEL_BOUNDARY, '\1 \2')
    spaced.split(SEPARATORS)
          .map { |part| part.gsub(NON_ALNUM, '') }
          .reject(&:empty?) # filter(Boolean) in the TS source
  end

  # Capitalize the first character and lowercase the rest.
  def cap(s)
    s.empty? ? '' : s[0].upcase + s[1..].downcase
  end

  # Title case: capitalize the first character of each maximal word-run.
  # Characters between runs (spacing, punctuation) are left untouched.
  def title(s)
    s.gsub(TITLE_WORD) { |run| cap(run) }
  end

  # Sentence case: lowercase + trim the whole string, then uppercase only the
  # first character. Empty input yields empty output.
  def sentence(s)
    t = s.downcase.strip
    t.empty? ? '' : t[0].upcase + t[1..]
  end

  def upper(s)
    s.upcase
  end

  def lower(s)
    s.downcase
  end

  # camelCase: the first word stays lowercase, every later word is capitalized.
  def camel(s)
    words(s).each_with_index.map { |w, i| i.zero? ? w.downcase : cap(w) }.join
  end

  # PascalCase: every word capitalized and concatenated.
  def pascal(s)
    words(s).map { |w| cap(w) }.join
  end

  def snake(s)
    words(s).join('_').downcase
  end

  def kebab(s)
    words(s).join('-').downcase
  end

  def constant(s)
    words(s).join('_').upcase
  end

  def slug(s)
    words(s).join('-').downcase
  end

  # The case-transform registry: CaseId -> proc. Keys mirror the TypeScript
  # caseTransforms object 1:1 (insertion order preserved, frozen).
  CASE_TRANSFORMS = {
    'upper'    => ->(s) { upper(s) },
    'lower'    => ->(s) { lower(s) },
    'title'    => ->(s) { title(s) },
    'sentence' => ->(s) { sentence(s) },
    'camel'    => ->(s) { camel(s) },
    'pascal'   => ->(s) { pascal(s) },
    'snake'    => ->(s) { snake(s) },
    'kebab'    => ->(s) { kebab(s) },
    'constant' => ->(s) { constant(s) },
    'slug'     => ->(s) { slug(s) }
  }.freeze

  # Valid case identifiers, in registry order.
  CASE_IDS = CASE_TRANSFORMS.keys.freeze

  # Apply a named case transform.
  #
  # Raises ArgumentError if +case_id+ is not a known case.
  def convert(case_id, s)
    fn = CASE_TRANSFORMS[case_id]
    raise ArgumentError, "Unknown case: #{case_id}" if fn.nil?

    fn.call(s)
  end

  # Like +convert+ but returns nil for an unknown case id.
  def try_convert(case_id, s)
    CASE_TRANSFORMS[case_id]&.call(s)
  end
end

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →