In Ruby, how do you check which version is running, and why is comparing RUBY_VERSION strings with < unreliable?
answer
- a frozen String, not a number
- character-by-character comparison
- "3.4.10" vs "3.4.9"
- Gem::Version compares numeric segments
- prereleases sort before the release
basics
~10 sRUBY_VERSION holds the running version as a String such as "4.0.7". Comparing it with < compares characters, so "3.4.10" < "3.4.9" is true; wrap both sides in Gem::Version, which compares numeric segments.
solid answer
~30 s`RUBY_VERSION` is a frozen String like `"4.0.7"` (major.minor.teeny); `ruby -v` prints it with build details. Strings compare **character by character**, so `"3.4.10" < "3.4.9"` is `true` because `"1"` sorts before `"9"`, and 3.4 really has shipped teeny releases past 9. The fix is `Gem::Version`, which splits the string into segments and compares them as numbers: `Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4")`. `Gem.ruby_version` already returns the running Ruby as a `Gem::Version`. Gem::Version also treats a segment with letters as a **prerelease**, so `4.0.0.preview2` sorts before `4.0.0`. For a single feature, checking that the feature exists can be clearer than any version check.
code
ruby · 10 linesRUBY_VERSION # => "4.0.7"
RUBY_VERSION.frozen? # => true
"3.4.10" < "3.4.9" # => true, string order
running = Gem.ruby_version # a Gem::Version
running >= Gem::Version.new("3.4") # => true
Gem::Version.new("3.4.10") > Gem::Version.new("3.4.9") # => true
Gem::Version.new("4.0.0.preview2") < Gem::Version.new("4.0.0") # => truego deeper
Recall that RUBY_VERSION is a String such as "4.0.7", that ruby -v prints it, and that version comparisons go through Gem::Version.
Explain why String#<=> gets "3.4.10" versus "3.4.9" wrong, how Gem::Version compares numeric segments, and how prereleases sort.
Prefer detecting a feature over checking a version when possible, and keep unavoidable version guards in one place so an upgrade removes them in one change.
Treat scattered RUBY_VERSION guards as upgrade debt: set a supported-version floor per codebase and delete branches for versions below it.
## What RUBY_VERSION is Every Ruby process defines the constant **`RUBY_VERSION`**: a frozen `String` holding the version of the Ruby language being run, in the form **major.minor.teeny**, for example `"4.0.7"`. `ruby -v` prints the same number together with the release date, commit, platform and markers such as `+PRISM`; the full line is also available as `RUBY_DESCRIPTION`. The three parts mean different things: - **major.minor** (`4.0`) - the yearly feature release. Ruby's own headers note that the minor version "changes annually", and that a major bump such as 3.4 to 4.0 is not a rewrite; for compatibility purposes a major and a minor bump are treated the same. - **teeny** (`7`) - bug-fix and security releases on that branch, `4.0.1`, `4.0.2` and so on. Ruby 4.0 also exposes the value as `Ruby::VERSION`, but `RUBY_VERSION` works on every version and is what most code uses. ## Why string comparison is wrong `RUBY_VERSION` is a String, so `<`, `>=` and `sort` use `String#<=>`, which compares **character by character**: ```ruby "3.4.10" < "3.4.9" # => true ("1" sorts before "9") "10.0.0" < "9.0.0" # => true "4.0.7" >= "3.4" # => true, but only by luck ``` This is not hypothetical: the 3.4 branch has shipped teeny releases 3.4.10 and 3.4.11, so a guard such as `RUBY_VERSION >= "3.4.9"` silently fails on a newer 3.4. Converting to a number does not help either: `RUBY_VERSION.to_f` drops the teeny part, and `to_i` keeps only the major number, so `"3.4.1".to_i >= 3.4` is `false`. ## Gem::Version: the comparison RubyGems itself uses RubyGems, which every normal Ruby process loads, provides **`Gem::Version`**. It splits a version string into segments and compares them one by one as numbers, so `3.10` is greater than `3.2`: 1. Build both sides: `Gem::Version.new(RUBY_VERSION)` and `Gem::Version.new("3.4")`. 2. Compare with `<`, `>=` and friends; `Gem::Version` includes `Comparable`. 3. Or start from `Gem.ruby_version`, which already returns the running Ruby as a `Gem::Version`. Details worth knowing: - **Prereleases.** A segment containing letters marks a prerelease, which sorts before the final release: `4.0.0.preview2 < 4.0.0`. A dash is normalised, so `"4.0.0-preview2"` becomes `4.0.0.pre.preview2`. - **Trailing zeros.** `Gem::Version.new("4.0") == Gem::Version.new("4.0.0")` is `true`, although `eql?` is `false` because the precision differs. - **Bad input.** `Gem::Version.new("abc")` raises `ArgumentError` ("Malformed version number string"). - **Development builds.** On a preview or trunk build, `Gem.ruby_version` appends the prerelease tag from `RUBY_DESCRIPTION`, so a preview of 4.0.0 compares below the final 4.0.0 while `RUBY_VERSION` alone reads `"4.0.0"`. | Expression | Result | Why | |---|---|---| | `"3.4.10" < "3.4.9"` | `true` | String comparison, character by character | | `Gem::Version.new("3.4.10") < Gem::Version.new("3.4.9")` | `false` | numeric segments: 10 > 9 | | `Gem::Version.new("4.0.0.preview2") < Gem::Version.new("4.0.0")` | `true` | prerelease sorts first | | `Gem.ruby_version >= Gem::Version.new("3.4")` | `true` on 4.0.7 | already a Gem::Version | ## Reading ruby -v The command-line view carries more than the number. A CRuby 4.0.7 build prints a line of the form `ruby 4.0.7 (<release date> revision <commit>) +PRISM [x86_64-linux]`: the version, the release date and the source revision, markers for active options such as `+PRISM` (the default parser since 3.4) or `+YJIT` when a JIT is on, and the platform in brackets. When a bug report says "Ruby 4.0", ask for this line; it answers which teeny release, which build and which options at once. Common mistakes in version checks: - Comparing `RUBY_VERSION` with a String literal, as above. - Checking `RUBY_ENGINE_VERSION` for language features; on JRuby or TruffleRuby that is the engine's own release number. - Scattering the same guard across many files, so an upgrade has to hunt for all of them. ## When not to compare versions at all A version check encodes an assumption: "3.4 has X". When the question really is "does this method or class exist?", testing for it states the intent directly and keeps working on engines whose `RUBY_VERSION` lags CRuby. Version checks fit best for **behaviour changes** that cannot be detected, such as a changed default or message format. ## What interviewers listen for They want `RUBY_VERSION` named as a String, the `"3.4.10"` trap explained, and `Gem::Version` or `Gem.ruby_version` as the fix.
- Why might Gem.ruby_version differ from Gem::Version.new(RUBY_VERSION)?On release builds they are equal. On a development or preview build, where `RUBY_PATCHLEVEL` is -1, `Gem.ruby_version` appends the prerelease tag it reads from `RUBY_DESCRIPTION`, so a preview of 4.0.0 compares below the final 4.0.0. `RUBY_VERSION` alone would read `"4.0.0"` for both.
- Does Gem::Version.new("4.0") equal Gem::Version.new("4.0.0")?With `==`, yes: comparison ignores trailing zero segments, so `<=>` returns 0. With `eql?`, no: RubyGems documents that a version is `eql?` only to one written at the same precision, so the two would be separate Hash keys.
saying these in an interview costs you the question
- RUBY_VERSION >= "3.4.9" is a safe guard for every later 3.4 release
- RUBY_VERSION.to_f is an accurate way to compare Ruby versions
- RUBY_VERSION is an Integer or Float you can compare numerically
- Gem::Version sorts 4.0.0.preview2 after the final 4.0.0
- Gem::Version compares versions as plain strings