Ruby 2.4
- Released at: Dec 25, 2016 (NEWS file)
- Status (as of Dec 25, 2023): EOL, latest is 2.4.10
- This document first published: Oct 14, 2019
- Last change to this document: Dec 25, 2023
Highlights
- Toplevel
return
- Unification of
Fixnum
andBignum
intoInteger
- Full Unicode support for String case operations
Warning
module for fine-grained warnings output controlComparable#clamp
Language
Multiple assignment allowed in conditional expression
- Reason: It is not what typically considered good style, but multiple assignment in conditions is now consistent with outside of condition behavior.
- Discussion: Feature #10617
- Documentation: —
- Code:
points = [] if (x, y = points.first) # in Ruby <2.4, will raise SyntaxError: multiple assignment in conditional p [x, y] end
- Note: Be aware that condition considered false if the whole right hand of assignment is
false
/nil
, not the first assigned variable:points = [[]] if (x, y = points.first) # x is nil, y is nil, but condition is [], which is truthy p [x, y] # will print this end
Toplevel return
return
statement at the top level of any .rb
file stops further execution of this file.
- Reason: Useful for code that depends on platform, presence of third-party libraries and so on;
return unless some_condition
at the beginning of the file will allow writing the rest of the code in assumption that necessary condition satisfied. - Discussion: Feature #4840
- Documentation: —
- Code:
# Some nokogiri_patch.rb # In Ruby < 2.4: if defined? Nokogiri # the rest of the code should all be nested in the unless end # Ruby 2.4 return unless defined? Nokogiri # The rest of the code can be written at the top level, now.
Refinements improvements
NB: Some of those changes are language-level, some are just method changes, but all a related to refinements.
Refinements are supported in Symbol#to_proc
and send
- Discussions: Feature #9451, Feature #11476
- Code:
module Tests refine Numeric do def normalize100 clamp(0, 100) end end end using Tests p [1, 700, 132].map(&:normalize100) # Ruby 2.3: undefined method `normalize100' for 1 # Ruby 2.4: => [1, 100, 100] p 123.send(:normalize100) # Ruby 2.3: undefined method `normalize100' for 1 # Ruby 2.4: => 100
refine
can refine modules, too
Previously, only classes could’ve been refined.
- Discussion: Feature #12534
- Documentation:
Module#refine
- Code:
module Tests refine Enumerable do # in 2.3: wrong argument type Module (expected Class) def tally each_with_object(Hash.new(0)) { |el, counter| counter[el] += 1} end end end using Tests p [1, 3, 1, 2, 1, 3].tally # => {1=>3, 3=>2, 2=>1}
Module.used_modules
Returns an array of all modules used in the current scope.
- Discussion: Feature #7418
- Documentation:
Module.used_modules
- Code:
module First refine Enumerable do end end module Second refine Object do end end module NoRefinements end p Module.used_modules # => [] using First using Second using NoRefinements p Module.used_modules # => [First, Second] -- note NoRefinements absence
Note that order of modules in array returned is not guaranteed.
Core
Warning
module
New single-method module was introduced, meant to be overridden in order to control warnings issued by Ruby.
- Reason:
- Discussion: Feature #12299
- Documentation: (introduced in 2.4, but documented in 2.5)
Warning
- Code:
def Warning.warn(msg) puts ".warn called with: #{msg.inspect}" end X = 1 X = 2 # Prints: # .warn called with: "<location>: warning: already initialized constant X\n" # .warn called with: "<location>: warning: previous definition of X was here\n"
- Follow-ups:
- Surprisingly as at may be,
Kernel#warn
haven’t been changed to callWarning.warn
in 2.4, but it was fixed in 2.5:def Warning.warn(msg) puts ".warn called with: #{msg.inspect}" end warn 'foo', 'bar' # Ruby 2.4 prints: # foo # bar # Ruby 2.5 prints: # .warn called with: "foo\nbar\n"
- In Ruby 2.7, new methods were added to
Warning
module allowing control over per-category warning suppression; - In Ruby 3.0, category support was improved.
- 3.3: added new warning category:
:performance
.
- Surprisingly as at may be,
Object#clone(freeze: false)
Allows to receive unfrozen copy of a frozen object.
- Reason: Previously, there was no way to receive an unfrozen copy of the frozen object, including its singleton class:
.dup
returns unfrozen object, but doesn’t copies the singleton class, while.clone
copies both (singleton class & frozen state). - Discussion: Feature #12300
- Documentation:
Object#clone
- Code:
h = {breed: 'Dog', name: 'Rex'} class << h def bark puts "Bark! Bark!" end end h.freeze h.bark # "Bark! Bark!" d = h.dup d.frozen? # => false d.bark # NoMethodError: undefined method `bark' h2 = h.clone(freeze: false) h2[:age] = 8 h2.bark # "Bark! Bark!"
- Note: Surprisingly enough,
unfrozen_object.clone(freeze: true)
doesn’t make object frozen. See discussion. - Follow-up: since 3.0:
clone(freeze: true)
works as expected;freeze:
argument is passed toinitialize_clone
so the object could properly freeze/unfreeze its internal data.
Comparable#clamp
Method to limit any comparable value to min
—max
range
- Discussion: Feature #10594
- Documentation:
Comparable#clamp
- Code:
123.clamp(0, 20) # => 20 -123.clamp(0, 20) # => 0 18.clamp(0, 20) # => 18
- Follow-up: In Ruby 2.7,
clamp
also allows passing range argument, which, especially when combined with endless (2.6) and beginless (2.7) ranges, allows to use more powerful and idiomatic code:123.clamp(0..20) # => 20 123.clamp(..20) # => 20 -123.clamp(0..) # => 0
Numerics
Fixnum
and Bignum
are unified into Integer
Historically, Ruby had two subclasses of Integer
: Fixnum
for numbers that fit into machine word, and Bignum
for larger numbers. Since Ruby 2.4, there is only one Integer
; Fixnum
and Bignum
are defined as (deprecated) constants synonymous to it.
- Reason:
Fixnum
/Bignum
separation always been an implementation detail, which led to confusion and sudden bugs, now this detail is hidden by interpreter. - Discussion: Feature #12005
- Documentation:
Integer
- Code:
# Before 2.4.0 10.class # => Fixnum (10**100).class # => Bignum # 2.4+ 10.class # => Integer (10**100).class # => Integer Fixnum # warning: constant ::Fixnum is deprecated # => Integer
Numeric#finite?
and #infinite?
- Reason: The methods were present in
Float
andBigDecimal
, but not in other numeric classes, which made it harder to write code uniformly processing numbers which may be integer/float/infinite. - Discussion: Feature #12039
- Documentation:
Numeric#infinite?
,Numeric#finite?
- Code:
1.infinite? # => nil 1.finite? # => true
- Note: Notice that
infinite?
returnsnil
/-1
/1
(alwaysnil
for integers), nottrue
/false
as most of other predicate methods. While unusual, it is convenient for checking both for infinity and its sign (+Infinity/-Infinity), and can be treated effectively astrue
/false
in boolean context.
Integer#digits
Returns an array of digits of the number.
- Reason: Useful for calculating checksums.
- Discussion: Feature #12447
- Documentation:
Integer.html#digits
- Code:
12345.digits # => [5, 4, 3, 2, 1] -- digits are returned in lowest-position-first order 0b11010.digits(2) # => [0, 1, 0, 1, 1] -- optional base can be passed
ndigits
optional argument for rounding methods
Rounding methods of numerics (ceil
/floor
etc.) now accept an optional argument to specify how many digits to truncate to. If argument is positive, it means decimal digits, and if it is negative, means tens (the same behavior #round
had since Ruby 1.9).
- Discussion: Feature #12245
- Documentation:
Numeric#ceil
,Numeric#floor
,Numeric#truncate
(in fact,Integer
andFloat
classes are affected, becauseRational
had the same option long ago). - Code:
123.4567.ceil(2) # => 123.46 123.4567.ceil(0) # => 124 123.4567.ceil(-1) # => 130
half:
option for #round
method
For numbers that are exact half, there are several options provided how to round them: up, down, or to the nearest even number. The default behavior kept unchanged (always up).
- Discussion: Bug #12958 (discussion of rounding behavior), Feature #12953 (additional rounding options)
- Documentation: (feature introduced in 2.4, but comprehensive docs were written in 2.5)
Integer#round
,Float#round
,Rational#round
- Code:
2.5.round # => 3 2.5.round(half: :down) # => 2 2.5.round(half: :even) # => 2 3.5.round(half: :even) # => 4 25.round(-1, half: :down) # => 20 (13/2r).round(half: :down) # => 6
Strings, symbols and regexps
See also chomp: option for
String#lines
and#each_line
, explained in IO section.
Unicode case conversions
All case-conversion methods for String
and Symbol
support full Unicode since 2.4.
- Discussion: Feature #10085
- Documentation:
String#downcase
,String#upcase
,String#capitalize
,String#swapcase
,Symbol#downcase
,Symbol#upcase
,Symbol#capitalize
,Symbol#swapcase
, - Code:
'Мамо, ДИВИСЬ, Unicode'.downcase # => "мамо, дивись, unicode" 'Мамо, ДИВИСЬ, Unicode'.downcase(:ascii) # => "Мамо, ДИВИСЬ, unicode" -- ASCII-only processing (old behavior) 'Straße'.downcase # => "straße" 'Straße'.downcase(:fold) # => "strasse" -- Unicode case folding 'TURKIC'.downcase # => "turkic" 'TURKIC'.downcase(:turkic) # => "turkıc" -- Turkic-specific "dotless i" conversion
String.new(capacity: size)
When string is created for usage as a mutable buffer for some large textual data, now expected size could be specified, thus optimizing memory allocations.
- Reason: Ruby’s mutable strings, when used for sequential building of some large text, cause constant reallocations of bigger and bigger memory buffer. By specifying expected capacity beforehand, one can avoid this reallocations.
- Discussion: Feature #12024
- Documentation:
String.new
- Code:
s = String.new(capacity: 10_000_000) # => "" -- it is still just an empty string, but internal buffer is already allocated large
- Note: Be careful about subtle difference in encoding, when constructing an empty string:
# Without source provided, default encoding is ASCII String.new(capacity: 10_000_000).encoding # => #<Encoding:ASCII-8BIT> # When explicitly constructed from empty string, has this string's encoding (defautl to UTF-8 for # string literals) String.new('', capacity: 10_000_000).encoding # => #<Encoding:UTF-8>
#casecmp?
In addition to long-existing case-insensitive comparison method String#casecmp(other)
(returning -1
, 0
, 1
like <=>
), new boolean String#casecmp?
and Symbol#casecmp?
were added.
- Discussion: Feature #
- Documentation:
String#casecmp?
,Symbol#casecmp?
- Code:
'test'.casecmp?('Test') # => true 'test'.casecmp?('Tset') # => false 'test'.casecmp?(:Test) # TypeError: no implicit conversion of Symbol into String
- Follow-up: In Ruby 2.5, behavior on incompatible types was changed to return
nil
, like==
does:'test' == :Test # => nil 'test'.casecmp?(:Test) # => nil
- Notes: It was proposed (in the discussion above), but never implemented to allow passing options to
casecmp?
, making it more precise by specifying locales. Currently, some local characters can produce unexpected results:'ı'.casecmp?('I') # => false, though it is Turkish small dotless "I" # ...but... 'ı'.upcase == 'I' # => true
String#concat
and #prepend
accept multiple arguments
- Discussion: Feature #12333
- Documentation:
String#concat
,String#prepend
- Code:
"Hello, ".concat('Judy', ', ', 'John', ' and ', 'Paul') # => "Hello, Judy, John and Paul" 'file.mp3'.prepend('dir1/', 'dir2/') # => "dir1/dir2/file.mp3"
String#unpack1
Just a shortcut for unpack(...).first
- Discussion: Feature #12752
- Documentation:
String#unpack1
- Code:
# Previously, you only could've get an Array "\x80".unpack('C') # => [128] # Since 2.4 "\x80".unpack1('C') # => 128
- Follow-up: In Ruby 3.1, a second argument was added to
unpack1
(andunpack
) allowing to unpack values from the middle of the string.
#match?
method
New boolean methods for checking if some pattern matches some string/symbol.
- Reason: In the (frequent) situation when only “matches or not” is important, boolean
match?
is more readable; also, it is more effective because doesn’t set global variables (in case ofRegexp#match?
) and doesn’t constructMatchData
. - Discussion: Feature #8110, Feature #12898
- Documentation:
Regexp#match?
,String#match?
,Symbol#match?
- Code:
# before 2.4 if username =~ /^Admin/ # Ruby 2.4: if username.match?(/^Admin/) # Also supports second parameter: position to search matches from: if username.match?(/:admin/, 3)
MatchData
: better support for named captures
MatchData#named_captures
returns the hash of {capture_name => captured string}
; MatchData#values_at
supports named captures.
- Discussion: Feature #11999, Feature #9179
- Documentation:
MatchData#named_captures
,MatchData#values_at
- Code:
m = 'Serhii Zhadan'.match(/^((?<first>.+?) (?<last>.+?))$/) # => #<MatchData "Serhii Zhadan" first:"Serhii" last:"Zhadan"> m.named_captures # => {"first"=>"Serhii", "last"=>"Zhadan"} m.values_at(:first, :last) # symbols are supported, too # => ["Serhii", "Zhadan"] m.values_at(0, :first, 2) # as well as a mix of named and numbered # => ["Serhii Zhadan", "Serhii", "Zhadan"]
Collections
Enumerable#chunk
without a block returns an Enumerator
- Discussion: Feature #2172
- Documentation:
Enumerable#chunk
- Code:
('a'..'k').chunk # => #<Enumerator: "a".."k":chunk> # Example of usage: ('a'..'k').chunk.with_index { |e, i| (i % 3).zero? }.to_a # => [ # [true, ["a"]], # [false, ["b", "c"]], # [true, ["d"]], # [false, ["e", "f"]], ...
#sum
Enumerable#sum
was implemented as a core alternative of too common reduce(:+)
- Discussion: Feature #12217
- Documentation:
Enumerable#sum
,Array#sum
- Code:
(1..5).sum # => 15 (1..5).sum { |x| x ** 2} # => 55 # Unlike reduce(:+), initial value is implicitly 0, so... [].reduce(:+) # => nil [].sum # => 0 ('a'..'f').reduce(:+) # => "abcdef" ('a'..'f').sum # TypeError: String can't be coerced into Integer ('a'..'f').sum('') # => "abcdef"
- Note: Separate implementation of
Array#sum
is provided for efficiency. Important thing to note is it doesn’t rely onArray#each
method:class MyAry < Array def each(&block) super { |val| yield val ** 2 } end end MyAry.new([1, 2, 3, 4, 5]).sum # => 15, not affected by reimplmented #each
#uniq
#uniq
method, previously present only in Array
, now available for Enumerable
and Enumerator::Lazy
- Discussion: Feature #11090
- Documentation:
Enumerable#iniq
,Enumerator::Lazy#uniq
- Code:
{a: 1, b: 2, c: 1, d: 2, e: 1}.uniq { |k, v| v } # => [[:a, 1], [:b, 2]] File.open('very_large_log.log').each_line.lazy.uniq { |ln| ln.scan(/Date: (\S+):/) }.take(10) # => first 10 of first-line-of-day
Array#max
and #min
#max
and #min
methods of Enumerable
reimplemented in Array
for speed.
- Discussion: Feature #12172
- Documentation:
Array#max
,Array#min
- Note: Beware that custom reimplementation of
Enumerable#max
and#min
are now ignored for arrays; and thatArray
’s implementation doesn’t use#each
method.
Array#concat
takes multiple arguments
- Discussion: Feature #12333
- Documentation:
Array#concat
- Code:
a = [1, 2] a.concat([3, 4], [5, 6]) # => [1, 2, 3, 4, 5, 6] a # => [1, 2, 3, 4, 5, 6]
Array#pack(buffer:)
When provided with optional buffer:
keyword argument, Array#pack
uses it as a receiver of data.
- Reason: a) pre-allocate the memory for big packed data and b) use the same buffer as a target for several chunks of data
- Discussion: Feature #12754
- Documentation:
Array#pack
- Code:
# Old way [82, 117, 98, 121].pack('C*') # => "Ruby" [32, 105, 115, 32, 99, 111, 111, 108, 33].pack('C*') # => " is cool!" # New way buffer = String.new(capacity: 30) [82, 117, 98, 121].pack('C*', buffer: buffer) # => "Ruby" buffer # => "Ruby" [32, 105, 115, 32, 99, 111, 111, 108, 33].pack('@4C*', buffer: buffer) # => "Ruby is cool!" buffer # => "Ruby is cool!" # Note that if the buffer already has content, the unpacked data is appended to it: [32, 73, 115, 32, 105, 116, 63].pack('C*', buffer: buffer) # => "Ruby is cool! Is it?" # It can be rewritten with explicit offset 0 directive: [32, 73, 115, 32, 105, 116, 63].pack('@0C*', buffer: buffer) # => " Is it?"
Hash#compact
and #compact!
Removes key-value pairs when value is nil
.
- Discussion: Feature #11818
- Documentation:
Hash#compact
,Hash#compact!
- Code:
data = {name: 'John', age: 34, occupation: nil} data.compact # => {:name=>"John", :age=>34} data # => {:name=>"John", :age=>34, :occupation=>nil} -- was not affected data.compact! # => {:name=>"John", :age=>34} data # => {:name=>"John", :age=>34} data.compact! # => nil -- if there were nothing to remove
- Note: Notice the last example: when destructive version haven’t changed a hash, it returns
nil
instead of hash itself. It is consistent with behavior of other destructive methods, and allows writing code like this:if data.compact! log.info 'Cleaned up the data' end
Hash#transform_values
and #transform_values!
Accepts block to transform each value of the hash.
- Reason: New method is much easier to write and read, and more effective than
hash.map { |key, val| [key, do_something(val)] }.to_h
- Discussion: Feature #12512
- Documentation:
Hash#transform_values
,Hash#transform_values!
- Code:
h = {x: '10', y: '12', z: '54'} h.transform_values(&:to_i) # => {:x=>10, :y=>12, :z=>54} h.transform_values!(&:to_i) # => {:x=>10, :y=>12, :z=>54} h # => {:x=>10, :y=>12, :z=>54} h.transform_values!(&:to_i) # => {:x=>10, :y=>12, :z=>54} -- unlike #compact!, always returns self # Without block, returns Enumerator h.transform_values # => #<Enumerator: {:x=>"10", :y=>"12", :z=>"54"}:transform_values> # Can be useful this way: h = {manager: 'Jane', reporter: 'John', qa: 'Jane', developer: 'Abraham'} h.transform_values.with_index(1) { |v, i| "#{i}: #{v}" } # => {:manager=>"1: Jane", :reporter=>"2: John", :qa=>"3: Jane", :developer=>"4: Abraham"}
- Follow-up: Ruby 2.5 also added
#transform_keys
and#transform_keys!
Filesystem and IO
chomp:
option for string splitting
In all contexts where input is split into lines, or received line-by-line, new optional keyword argument chomp: true
was added to remove (chomp) line-endings.
- Reason: Before this change, all line-by-line operations should’ve included
chomp
as a separate operations when line ending is not needed, which turned out to be most o the cases. - Discussion: Feature #12553
- Documentation (feature introduced in 2.4, but comprehensive docs were written in 2.5-2.6):
String#each_line
,String#lines
,IO#gets
,IO#readline
,IO#readlines
,IO.foreach
,IO.readlines
,- Standard library: methods of the class affected, but no documentation for the change:
StringIO#gets
,StringIO#each_line
,StringIO#readlines
- Code:
# The effect is the same with String, IO and StringIO, so we are demonstrating just one example: require 'stringio' io = StringIO.new("foo\nbar\nbaz\n") io.gets # => "foo\n" io.gets(chomp: true) # => "bar"
- Notes: What is chomped (and what is lines split on) is controlled by
$/
global variable (dubbed$RS
or$INPUT_RECORD_SEPARATOR
byEnglish
module). While quite esoteric by today’s standards, it could be really useful for one-off scripts that work with specific data:records = <<~DATA First line $$$ Second line $$$ Third line DATA $/ = "\n$$$\n" records.each_line(chomp: true).to_a # => ["First line", "Second line", "Third line"] # The same effect, though, can be achieve with normal `separator` method argument: records.each_line("\n$$$\n", chomp: true).to_a # => ["First line", "Second line", "Third line"]
#empty?
method for filesystem objects
New method empty?
was introduced into several classes to check if the file/directory is empty.
- Discussion: Feature #10121 (
Dir
), Feature #9969 (File
), Feature #12596 (Pathname
) - Documentation:
Dir#empty?
,File#empty?
, (stdlib)Pathname#empty?
- Code:
Dir.empty?('emptydir') # => true Dir.empty?('nonemptydir') # => false Dir.empty?('nonexistent') # Errno::ENOENT (No such file or directory @ rb_dir_s_empty_p - nonexistent) Dir.empty?('file') # => false File.empty?('emptyfile') # => true File.empty?('nonemptyfile') # => false File.empty?('nonexistent') # => false -- unlike Dir.empty? File.empty?('dir') # => false require 'pathname' Pathname('emptydir').empty? # => true Pathname('emptyfile').empty? # => true Pathname('nonexistent').empty? # => false Pathname('nonempty').empty? # => false
Thread#report_on_exception
and Thread.report_on_exception
Global and thread-local boolean flag to set what should the thread do when ended with exception: die silently (default, old behavior) or print the exception and backtrace to $stderr
.
- Reason: Threads silently dying without any indication could be a lot of confusion, and before this feature top-level exception reporting for each thread should’ve been implemented manually.
- Discussion: Feature #6647
- Documentation:
Thread.report_on_exception
,Thread.report_on_exception=
,Thread#report_on_exception
,Thread#report_on_exception
- Code:
Thread.new { puts 1 / 0 } sleep(1) # => nothing happens, thread is dead Thread.report_on_exception = true Thread.new { puts 1 / 0 } sleep(1) # #<Thread:0x0055b070475fb0> terminated with exception: # in `/': divided by 0 (ZeroDivisionError) # Or instance-level method: t = Thread.new { sleep(1); puts 1 / 0 } t.report_on_exception = false # silence it again sleep(1) # => Thread dies in a sad silence.
- Follow-up: Since 2.5,
Thread.report_on_exception
istrue
by default.
TracePoint#callee_id
Returns an actual name of the method being called, even if aliased.
- Discussion: Feature #12747
- Documentation:
TracePoint#callee_id
- Code:
tp = TracePoint.new(:call) { |point| p [point.method_id, point.callee_id]} def real end alias aliased real tp.enable { aliased } # prints method id and callee id: [:real, :aliased]
Stdlib
Set
:#compare_by_identity
and#compare_by_identity?
methods added, behaving the same way as (existing since 1.9)Hash#compare_by_identity
: only elements being the same object (same#object_id
) are considered same set element. Discussion: Feature #12210CSV.new
: Add aliberal_parsing
option, allowing to (try to) parse not-completely-valid CSV. Discussion: Feature #11839Binding#irb
start a REPL session likebinding.pry
.Follow-up: since Ruby 2.5,require 'irb'
is not necessary for the feature to work, it is done automatically.Logger.new
adds keyword argumentslevel:
,progname:
,datetime_format:
,formatter:
,shift_period_suffix:
. The latter allows specifying suffix for filenames on log rotation. Discussions: Feature #12224 (keyword args), Feature #10772 (shift_period_suffix
).Net::HTTP.post
shortcut method. Discussion: Feature #12375Net::FTP
:- Support TLS.
- Support hash style options for
Net::FTP.new
. While not reflected in the docs, “old” way (as itwas
before 2.4, with separate args for user, password etc.) still works for backwards compatibility. Net::FTP#status
: optional argumentpathname
(STAT path
“is analogous to the “list” command, except that data shall be transferred over the control connection”). Discussion: Feature #12965
OptionParser
(optparse):OptionParser#parse!
and similar methods addinto:
option to parse, greatly simplifying trivial case of “parse into hash”. Discussion: Feature #11191require 'optparse' opts = OptionParser.new do |o| o.on '-p', '--port=PORT', 'port', Integer o.on '-v', '--verbose' end result = {} opts.parse!(%w[-p 8080 -v], into: result) p result # => {:port=>8080, :verbose=>true}
Readline
:::quoting_detection_proc
andquoting_detection_proc=
to specify callable object (Proc
or anything responding to#call
), customizing the decision “if in this line, character in that position is quoted or not”. It is standard functionality of GNU readline which was not previously exposed by Ruby wrapper. Discussion: Feature #12659
Libraries promoted to default/bundled gems
stdgems.org project has a nice explanations of default and bundled gems concepts, as well as a list of currently gemified libraries.
“For the rest of us”:
- default means libraries development extracted into separate GitHub repositories, and they are just packaged with main Ruby before release. It means you can do issue/PR to any of them independently, without going through more tough development process of the core Ruby;
- bundled means libraries development also extracted, and they are not packaged with Ruby distribution, just automatically installed with it.
Libraries that became default in 2.4:
Libraries that became bundled in 2.4:
Follow-up:
- 16 more libraries gemified in 2.5;
- 14 more libraries gemified in 2.6;
- 16 more libraries gemified in 2.7, and 6 just dropped from the standard library;
- 34 (!) more libraries gemified in 3.0, and 3 more just dropped from the standard library (including xmlrpc and webrick).