Merge pull request #4920 from reitermarkus/days
Use ActiveSupport’s `#days`.
This commit is contained in:
commit
7cb1ceb756
@ -5,9 +5,7 @@ end
|
||||
std_trap = trap("INT") { exit! 130 } # no backtrace thanks
|
||||
|
||||
# check ruby version before requiring any modules.
|
||||
RUBY_VERSION_SPLIT = RUBY_VERSION.split "."
|
||||
RUBY_X = RUBY_VERSION_SPLIT[0].to_i
|
||||
RUBY_Y = RUBY_VERSION_SPLIT[1].to_i
|
||||
RUBY_X, RUBY_Y, = RUBY_VERSION.split(".").map(&:to_i)
|
||||
if RUBY_X < 2 || (RUBY_X == 2 && RUBY_Y < 3)
|
||||
raise "Homebrew must be run under Ruby 2.3! You're running #{RUBY_VERSION}."
|
||||
end
|
||||
|
||||
@ -115,10 +115,8 @@ module Cask
|
||||
command.run!("/bin/launchctl", args: ["list"]).stdout.lines
|
||||
.map { |line| line.chomp.split("\t") }
|
||||
.map { |pid, state, id| [pid.to_i, state.to_i, id] }
|
||||
.select do |fields|
|
||||
next if fields[0].zero?
|
||||
|
||||
fields[2] =~ /^#{Regexp.escape(bundle_id)}($|\.\d+)/
|
||||
.select do |(pid, _, id)|
|
||||
pid.nonzero? && id.match?(/^#{Regexp.escape(bundle_id)}($|\.\d+)/)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@ module Cask
|
||||
end
|
||||
|
||||
def audit_languages(languages)
|
||||
ohai "Auditing language: #{languages.map { |lang| "'#{lang}'" }.join(", ")}"
|
||||
ohai "Auditing language: #{languages.map { |lang| "'#{lang}'" }.to_sentence}"
|
||||
MacOS.instance_variable_set(:@languages, languages)
|
||||
audit_cask_instance(CaskLoader.load(cask.sourcefile_path))
|
||||
ensure
|
||||
|
||||
@ -32,7 +32,7 @@ module Cask
|
||||
rescue TSort::Cyclic
|
||||
strongly_connected_components = graph.strongly_connected_components.sort_by(&:count)
|
||||
cyclic_dependencies = strongly_connected_components.last - [cask]
|
||||
raise CaskCyclicDependencyError.new(cask.token, cyclic_dependencies.join(", "))
|
||||
raise CaskCyclicDependencyError.new(cask.token, cyclic_dependencies.to_sentence)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -182,10 +182,14 @@ module Cask
|
||||
end
|
||||
|
||||
def self.cask_count_for_tap(tap)
|
||||
Formatter.pluralize(tap.cask_files.count, "cask")
|
||||
rescue
|
||||
add_error "Unable to read from Tap: #{tap.path}"
|
||||
"0"
|
||||
cask_count = begin
|
||||
tap.cask_files.count
|
||||
rescue
|
||||
add_error "Unable to read from Tap: #{tap.path}"
|
||||
0
|
||||
end
|
||||
|
||||
"#{cask_count} #{"cask".pluralize(cask_count)}"
|
||||
end
|
||||
|
||||
def self.render_env_var(var)
|
||||
|
||||
@ -23,11 +23,9 @@ module Cask
|
||||
|
||||
next if (versions = cask.versions).empty?
|
||||
|
||||
single = versions.count == 1
|
||||
|
||||
puts <<~EOS
|
||||
#{cask} #{versions.join(", ")} #{single ? "is" : "are"} still installed.
|
||||
Remove #{single ? "it" : "them all"} with `brew cask uninstall --force #{cask}`.
|
||||
#{cask} #{versions.to_sentence} #{"is".pluralize(versions.count)} still installed.
|
||||
Remove #{(versions.count == 1) ? "it" : "them all"} with `brew cask uninstall --force #{cask}`.
|
||||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
@ -28,7 +28,7 @@ module Cask
|
||||
end
|
||||
|
||||
ohai "Casks with `auto_updates` or `version :latest` will not be upgraded" if args.empty? && !greedy?
|
||||
oh1 "Upgrading #{Formatter.pluralize(outdated_casks.length, "outdated package")}, with result:"
|
||||
oh1 "Upgrading #{outdated_casks.count} #{"outdated package".pluralize(outdated_casks.count)}:"
|
||||
cask_upgrades = outdated_casks.map do |cask|
|
||||
if cask.installed_caskfile.nil?
|
||||
"#{cask.full_name} #{cask.version}"
|
||||
|
||||
@ -89,19 +89,19 @@ module Cask
|
||||
end
|
||||
|
||||
def before_comma
|
||||
version { split(",", 2)[0] }
|
||||
version { split(",", 2).first }
|
||||
end
|
||||
|
||||
def after_comma
|
||||
version { split(",", 2)[1] }
|
||||
version { split(",", 2).second }
|
||||
end
|
||||
|
||||
def before_colon
|
||||
version { split(":", 2)[0] }
|
||||
version { split(":", 2).first }
|
||||
end
|
||||
|
||||
def after_colon
|
||||
version { split(":", 2)[1] }
|
||||
version { split(":", 2).second }
|
||||
end
|
||||
|
||||
def no_dividers
|
||||
|
||||
@ -4,7 +4,7 @@ require "cask/cask_loader"
|
||||
require "set"
|
||||
|
||||
module CleanupRefinement
|
||||
LATEST_CASK_DAYS = 7
|
||||
LATEST_CASK_OUTDATED = 7.days.ago
|
||||
|
||||
refine Enumerator do
|
||||
def parallel
|
||||
@ -51,8 +51,7 @@ module CleanupRefinement
|
||||
|
||||
return true if symlink? && !exist?
|
||||
|
||||
# TODO: Replace with ActiveSupport's `.days.ago`.
|
||||
mtime < ((@time ||= Time.now) - days * 60 * 60 * 24)
|
||||
mtime < days.days.ago
|
||||
end
|
||||
|
||||
def stale?(scrub = false)
|
||||
@ -124,10 +123,7 @@ module CleanupRefinement
|
||||
|
||||
return true if scrub && !cask.versions.include?(cask.version)
|
||||
|
||||
if cask.version.latest?
|
||||
# TODO: Replace with ActiveSupport's `.days.ago`.
|
||||
return mtime < ((@time ||= Time.now) - LATEST_CASK_DAYS * 60 * 60 * 24)
|
||||
end
|
||||
return mtime < LATEST_CASK_OUTDATED if cask.version.latest?
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
@ -18,17 +18,17 @@ module Homebrew
|
||||
|
||||
case ARGV.named.first
|
||||
when nil, "state"
|
||||
analyticsdisabled = \
|
||||
Utils.popen_read("git config --file=#{config_file} --get homebrew.analyticsdisabled").chuzzle
|
||||
uuid = \
|
||||
Utils.popen_read("git config --file=#{config_file} --get homebrew.analyticsuuid").chuzzle
|
||||
analyticsdisabled =
|
||||
Utils.popen_read("git config --file=#{config_file} --get homebrew.analyticsdisabled").chomp
|
||||
uuid =
|
||||
Utils.popen_read("git config --file=#{config_file} --get homebrew.analyticsuuid").chomp
|
||||
if ENV["HOMEBREW_NO_ANALYTICS"]
|
||||
puts "Analytics is disabled (by HOMEBREW_NO_ANALYTICS)."
|
||||
elsif analyticsdisabled == "true"
|
||||
puts "Analytics is disabled."
|
||||
else
|
||||
puts "Analytics is enabled."
|
||||
puts "UUID: #{uuid}" if uuid
|
||||
puts "UUID: #{uuid}" if uuid.present?
|
||||
end
|
||||
when "on"
|
||||
safe_system "git", "config", "--file=#{config_file}",
|
||||
|
||||
@ -49,7 +49,7 @@ module Homebrew
|
||||
if ARGV.named.empty?
|
||||
if HOMEBREW_CELLAR.exist?
|
||||
count = Formula.racks.length
|
||||
puts "#{Formatter.pluralize(count, "keg")}, #{HOMEBREW_CELLAR.abv}"
|
||||
puts "#{count} #{"keg".pluralize(count)}, #{HOMEBREW_CELLAR.abv}"
|
||||
end
|
||||
else
|
||||
ARGV.named.each_with_index do |f, i|
|
||||
|
||||
@ -28,7 +28,7 @@ module Homebrew
|
||||
.map { |d| Keg.new(d).version }
|
||||
.sort
|
||||
.join(", ")
|
||||
version = ARGV[1]
|
||||
version = ARGV.second
|
||||
|
||||
if !version || ARGV.named.length > 2
|
||||
onoe usage
|
||||
|
||||
@ -48,11 +48,11 @@ module Homebrew
|
||||
pinned_count += 1 if tap.pinned?
|
||||
private_count += 1 if tap.private?
|
||||
end
|
||||
info = Formatter.pluralize(tap_count, "tap").to_s
|
||||
info = "#{tap_count} #{"tap".pluralize(tap_count)}"
|
||||
info += ", #{pinned_count} pinned"
|
||||
info += ", #{private_count} private"
|
||||
info += ", #{Formatter.pluralize(formula_count, "formula")}"
|
||||
info += ", #{Formatter.pluralize(command_count, "command")}"
|
||||
info += ", #{formula_count} #{"formula".pluralize(formula_count)}"
|
||||
info += ", #{command_count} #{"command".pluralize(command_count)}"
|
||||
info += ", #{Tap::TAP_DIRECTORY.abv}" if Tap::TAP_DIRECTORY.directory?
|
||||
puts info
|
||||
else
|
||||
|
||||
@ -45,9 +45,9 @@ module Homebrew
|
||||
elsif ARGV.named.empty?
|
||||
puts Tap.names
|
||||
else
|
||||
tap = Tap.fetch(ARGV.named[0])
|
||||
tap = Tap.fetch(ARGV.named.first)
|
||||
begin
|
||||
tap.install clone_target: ARGV.named[1],
|
||||
tap.install clone_target: ARGV.named.second,
|
||||
force_auto_update: force_auto_update?,
|
||||
full_clone: full_clone?,
|
||||
quiet: ARGV.quieter?
|
||||
|
||||
@ -66,8 +66,7 @@ module Homebrew
|
||||
|
||||
if rack.directory?
|
||||
versions = rack.subdirs.map(&:basename)
|
||||
verb = Formatter.pluralize(versions.length, "is", "are")
|
||||
puts "#{keg.name} #{versions.join(", ")} #{verb} still installed."
|
||||
puts "#{keg.name} #{versions.to_sentence} #{"is".pluralize(versions.count)} still installed."
|
||||
puts "Remove all versions with `brew uninstall --force #{keg.name}`."
|
||||
end
|
||||
end
|
||||
@ -119,31 +118,20 @@ module Homebrew
|
||||
|
||||
protected
|
||||
|
||||
def are(items)
|
||||
Formatter.pluralize(items.count, "is", "are", show_count: false)
|
||||
end
|
||||
|
||||
def they(items)
|
||||
Formatter.pluralize(items.count, "it", "they", show_count: false)
|
||||
end
|
||||
|
||||
def list(items)
|
||||
items.join(", ")
|
||||
end
|
||||
|
||||
def sample_command
|
||||
"brew uninstall --ignore-dependencies #{ARGV.named.join(" ")}"
|
||||
end
|
||||
|
||||
def are_required_by_deps
|
||||
"#{are reqs} required by #{list deps}, which #{are deps} currently installed"
|
||||
"#{"is".pluralize(reqs.count)} required by #{deps.to_sentence}, " \
|
||||
"which #{"is".pluralize(deps.count)} currently installed"
|
||||
end
|
||||
end
|
||||
|
||||
class DeveloperDependentsMessage < DependentsMessage
|
||||
def output
|
||||
opoo <<~EOS
|
||||
#{list reqs} #{are_required_by_deps}.
|
||||
#{reqs.to_sentence} #{are_required_by_deps}.
|
||||
You can silence this warning with:
|
||||
#{sample_command}
|
||||
EOS
|
||||
@ -153,8 +141,8 @@ module Homebrew
|
||||
class NondeveloperDependentsMessage < DependentsMessage
|
||||
def output
|
||||
ofail <<~EOS
|
||||
Refusing to uninstall #{list reqs}
|
||||
because #{they reqs} #{are_required_by_deps}.
|
||||
Refusing to uninstall #{reqs.to_sentence}
|
||||
because #{"it".pluralize(reqs.count)} #{are_required_by_deps}.
|
||||
You can override this and force removal with:
|
||||
#{sample_command}
|
||||
EOS
|
||||
|
||||
@ -22,14 +22,14 @@ module Homebrew
|
||||
def update_report
|
||||
HOMEBREW_REPOSITORY.cd do
|
||||
analytics_message_displayed =
|
||||
Utils.popen_read("git", "config", "--local", "--get", "homebrew.analyticsmessage").chuzzle
|
||||
Utils.popen_read("git", "config", "--local", "--get", "homebrew.analyticsmessage").chomp == "true"
|
||||
cask_analytics_message_displayed =
|
||||
Utils.popen_read("git", "config", "--local", "--get", "homebrew.caskanalyticsmessage").chuzzle
|
||||
Utils.popen_read("git", "config", "--local", "--get", "homebrew.caskanalyticsmessage").chomp == "true"
|
||||
analytics_disabled =
|
||||
Utils.popen_read("git", "config", "--local", "--get", "homebrew.analyticsdisabled").chuzzle
|
||||
if analytics_message_displayed != "true" &&
|
||||
cask_analytics_message_displayed != "true" &&
|
||||
analytics_disabled != "true" &&
|
||||
Utils.popen_read("git", "config", "--local", "--get", "homebrew.analyticsdisabled").chomp == "true"
|
||||
if !analytics_message_displayed &&
|
||||
!cask_analytics_message_displayed &&
|
||||
!analytics_disabled &&
|
||||
!ENV["HOMEBREW_NO_ANALYTICS"] &&
|
||||
!ENV["HOMEBREW_NO_ANALYTICS_MESSAGE_OUTPUT"]
|
||||
|
||||
@ -53,8 +53,8 @@ module Homebrew
|
||||
end
|
||||
|
||||
donation_message_displayed =
|
||||
Utils.popen_read("git", "config", "--local", "--get", "homebrew.donationmessage").chuzzle
|
||||
if donation_message_displayed != "true"
|
||||
Utils.popen_read("git", "config", "--local", "--get", "homebrew.donationmessage").chomp == "true"
|
||||
unless donation_message_displayed
|
||||
ohai "Homebrew is run entirely by unpaid volunteers. Please consider donating:"
|
||||
puts " #{Formatter.url("https://github.com/Homebrew/brew#donations")}\n"
|
||||
|
||||
@ -107,8 +107,7 @@ module Homebrew
|
||||
|
||||
unless updated_taps.empty?
|
||||
update_preinstall_header
|
||||
puts "Updated #{Formatter.pluralize(updated_taps.size, "tap")} " \
|
||||
"(#{updated_taps.join(", ")})."
|
||||
puts "Updated #{updated_taps.count} #{"tap".pluralize(updated_taps.count)} (#{updated_taps.to_sentence})."
|
||||
updated = true
|
||||
end
|
||||
|
||||
|
||||
@ -63,14 +63,14 @@ module Homebrew
|
||||
formulae_to_install = outdated.map(&:latest_formula)
|
||||
|
||||
if !pinned.empty? && !ARGV.include?("--ignore-pinned")
|
||||
ofail "Not upgrading #{Formatter.pluralize(pinned.length, "pinned package")}:"
|
||||
ofail "Not upgrading #{pinned.count} pinned #{"package".pluralize(pinned.count)}:"
|
||||
puts pinned.map { |f| "#{f.full_specified_name} #{f.pkg_version}" } * ", "
|
||||
end
|
||||
|
||||
if formulae_to_install.empty?
|
||||
oh1 "No packages to upgrade"
|
||||
else
|
||||
oh1 "Upgrading #{Formatter.pluralize(formulae_to_install.length, "outdated package")}, with result:"
|
||||
oh1 "Upgrading #{formulae_to_install.count} outdated #{"package".pluralize(formulae_to_install.count)}:"
|
||||
formulae_upgrades = formulae_to_install.map do |f|
|
||||
if f.optlinked?
|
||||
"#{f.full_specified_name} #{Keg.new(f.opt_prefix).version} -> #{f.pkg_version}"
|
||||
@ -304,7 +304,7 @@ module Homebrew
|
||||
|
||||
# Print the pinned dependents.
|
||||
unless pinned.empty?
|
||||
ohai "Not upgrading #{Formatter.pluralize(pinned.length, "pinned dependent")}:"
|
||||
ohai "Not upgrading #{pinned.count} pinned #{"dependent".pluralize(pinned.count)}:"
|
||||
puts pinned.map { |f| "#{f.full_specified_name} #{f.pkg_version}" } * ", "
|
||||
end
|
||||
|
||||
@ -312,7 +312,7 @@ module Homebrew
|
||||
if upgradable.empty?
|
||||
ohai "No dependents to upgrade" if ARGV.verbose?
|
||||
else
|
||||
ohai "Upgrading #{Formatter.pluralize(upgradable.length, "dependent")}:"
|
||||
ohai "Upgrading #{upgradable.count} #{"dependent".pluralize(upgradable.count)}:"
|
||||
formulae_upgrades = upgradable.map do |f|
|
||||
if f.optlinked?
|
||||
"#{f.full_specified_name} #{Keg.new(f.opt_prefix).version} -> #{f.pkg_version}"
|
||||
@ -337,7 +337,7 @@ module Homebrew
|
||||
|
||||
# Print the pinned dependents.
|
||||
unless pinned.empty?
|
||||
onoe "Not reinstalling #{Formatter.pluralize(pinned.length, "broken and outdated, but pinned dependent")}:"
|
||||
onoe "Not reinstalling #{pinned.count} broken and outdated, but pinned #{"dependent".pluralize(pinned.count)}:"
|
||||
$stderr.puts pinned.map { |f| "#{f.full_specified_name} #{f.pkg_version}" } * ", "
|
||||
end
|
||||
|
||||
@ -345,7 +345,7 @@ module Homebrew
|
||||
if reinstallable.empty?
|
||||
ohai "No broken dependents to reinstall" if ARGV.verbose?
|
||||
else
|
||||
ohai "Reinstalling #{Formatter.pluralize(reinstallable.length, "broken dependent")} from source:"
|
||||
ohai "Reinstalling #{reinstallable.count} broken #{"dependent".pluralize(reinstallable.count)} from source:"
|
||||
puts reinstallable.map(&:full_specified_name).join(", ")
|
||||
end
|
||||
|
||||
|
||||
@ -6,9 +6,6 @@ using CleanupRefinement
|
||||
module Cask
|
||||
class Cmd
|
||||
class Cleanup < AbstractCommand
|
||||
OUTDATED_DAYS = 10
|
||||
OUTDATED_TIMESTAMP = Time.now - (60 * 60 * 24 * OUTDATED_DAYS)
|
||||
|
||||
def self.help
|
||||
"cleans up cached downloads and tracker symlinks"
|
||||
end
|
||||
|
||||
@ -186,9 +186,9 @@ module Homebrew
|
||||
end
|
||||
|
||||
total_problems_count = problem_count + new_formula_problem_count
|
||||
problem_plural = Formatter.pluralize(total_problems_count, "problem")
|
||||
formula_plural = Formatter.pluralize(formula_count, "formula")
|
||||
corrected_problem_plural = Formatter.pluralize(corrected_problem_count, "problem")
|
||||
problem_plural = "#{total_problems_count} #{"problem".pluralize(total_problems_count)}"
|
||||
formula_plural = "#{formula_count} #{"formula".pluralize(formula_count)}"
|
||||
corrected_problem_plural = "#{corrected_problem_count} #{"problem".pluralize(corrected_problem_count)}"
|
||||
errors_summary = "#{problem_plural} in #{formula_plural} detected"
|
||||
if corrected_problem_count.positive?
|
||||
errors_summary += ", #{corrected_problem_plural} corrected"
|
||||
@ -512,7 +512,7 @@ module Homebrew
|
||||
# Formulae names can legitimately be uppercase/lowercase/both.
|
||||
name = Regexp.new(formula.name, Regexp::IGNORECASE)
|
||||
reason.sub!(name, "")
|
||||
first_word = reason.split[0]
|
||||
first_word = reason.split.first
|
||||
|
||||
if reason =~ /\A[A-Z]/ && !reason.start_with?(*whitelist)
|
||||
problem <<~EOS
|
||||
@ -724,7 +724,7 @@ module Homebrew
|
||||
|
||||
version = Version.parse(stable.url)
|
||||
if version >= Version.create("1.0")
|
||||
minor_version = version.to_s.split(".", 3)[1].to_i
|
||||
_, minor_version, = version.to_s.split(".", 3).map(&:to_i)
|
||||
if minor_version.odd?
|
||||
problem "#{stable.version} is a development release"
|
||||
end
|
||||
|
||||
@ -120,7 +120,7 @@ module Homebrew
|
||||
# Expect exactly two named arguments: formula and tap
|
||||
raise UsageError if ARGV.named.length != 2
|
||||
|
||||
destination_tap = Tap.fetch(ARGV.named[1])
|
||||
destination_tap = Tap.fetch(ARGV.named.second)
|
||||
odie "Cannot extract formula to homebrew/core!" if destination_tap.core_tap?
|
||||
destination_tap.install unless destination_tap.installed?
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ module Homebrew
|
||||
).lines.first.chomp
|
||||
odie "Could not find any previous tags!" unless previous_tag
|
||||
|
||||
end_ref = ARGV.named[1] || "origin/master"
|
||||
end_ref = ARGV.named.second || "origin/master"
|
||||
|
||||
[previous_tag, end_ref].each do |ref|
|
||||
next if quiet_system "git", "-C", HOMEBREW_REPOSITORY, "rev-parse", "--verify", "--quiet", ref
|
||||
|
||||
@ -61,7 +61,7 @@ module Homebrew
|
||||
Utils.popen_read("git", "rev-list", "-n1", "--before=#{date}", "origin/master").chomp
|
||||
elsif args.to_tag?
|
||||
tags = Utils.popen_read("git", "tag", "--list", "--sort=-version:refname")
|
||||
previous_tag = tags.lines[1]
|
||||
previous_tag = tags.lines.second
|
||||
previous_tag ||= begin
|
||||
if (HOMEBREW_REPOSITORY/".git/shallow").exist?
|
||||
safe_system "git", "fetch", "--tags", "--depth=1"
|
||||
@ -69,7 +69,7 @@ module Homebrew
|
||||
elsif OS.linux?
|
||||
tags = Utils.popen_read("git tag --list | sort -rV")
|
||||
end
|
||||
tags.lines[1]
|
||||
tags.lines.second
|
||||
end
|
||||
previous_tag = previous_tag.to_s.chomp
|
||||
odie "Could not find previous tag in:\n#{tags}" if previous_tag.empty?
|
||||
|
||||
@ -105,7 +105,7 @@ module FormulaClassUnavailableErrorModule
|
||||
end
|
||||
|
||||
def format_list(class_list)
|
||||
class_list.map { |klass| klass.name.split("::")[-1] }.join(", ")
|
||||
class_list.map { |klass| klass.name.split("::").last }.join(", ")
|
||||
end
|
||||
end
|
||||
|
||||
@ -441,18 +441,10 @@ end
|
||||
# and are being installed on a system without necessary build tools
|
||||
class BuildToolsError < RuntimeError
|
||||
def initialize(formulae)
|
||||
if formulae.length > 1
|
||||
formula_text = "formulae"
|
||||
package_text = "binary packages"
|
||||
else
|
||||
formula_text = "formula"
|
||||
package_text = "a binary package"
|
||||
end
|
||||
|
||||
super <<~EOS
|
||||
The following #{formula_text}:
|
||||
#{formulae.join(", ")}
|
||||
cannot be installed as #{package_text} and must be built from source.
|
||||
The following #{"formula".pluralize(formulae.count)}
|
||||
#{formulae.to_sentence}
|
||||
cannot be installed as #{"binary package".pluralize(formulae.count)} and must be built from source.
|
||||
#{DevelopmentTools.installation_instructions}
|
||||
EOS
|
||||
end
|
||||
|
||||
@ -9,7 +9,7 @@ module GitRepositoryExtension
|
||||
def git_origin
|
||||
return unless git? && Utils.git_available?
|
||||
|
||||
Utils.popen_read("git", "config", "--get", "remote.origin.url", chdir: self).chuzzle
|
||||
Utils.popen_read("git", "config", "--get", "remote.origin.url", chdir: self).chomp.presence
|
||||
end
|
||||
|
||||
def git_origin=(origin)
|
||||
@ -21,30 +21,30 @@ module GitRepositoryExtension
|
||||
def git_head
|
||||
return unless git? && Utils.git_available?
|
||||
|
||||
Utils.popen_read("git", "rev-parse", "--verify", "-q", "HEAD", chdir: self).chuzzle
|
||||
Utils.popen_read("git", "rev-parse", "--verify", "-q", "HEAD", chdir: self).chomp.presence
|
||||
end
|
||||
|
||||
def git_short_head
|
||||
return unless git? && Utils.git_available?
|
||||
|
||||
Utils.popen_read("git", "rev-parse", "--short=4", "--verify", "-q", "HEAD", chdir: self).chuzzle
|
||||
Utils.popen_read("git", "rev-parse", "--short=4", "--verify", "-q", "HEAD", chdir: self).chomp.presence
|
||||
end
|
||||
|
||||
def git_last_commit
|
||||
return unless git? && Utils.git_available?
|
||||
|
||||
Utils.popen_read("git", "show", "-s", "--format=%cr", "HEAD", chdir: self).chuzzle
|
||||
Utils.popen_read("git", "show", "-s", "--format=%cr", "HEAD", chdir: self).chomp.presence
|
||||
end
|
||||
|
||||
def git_branch
|
||||
return unless git? && Utils.git_available?
|
||||
|
||||
Utils.popen_read("git", "rev-parse", "--abbrev-ref", "HEAD", chdir: self).chuzzle
|
||||
Utils.popen_read("git", "rev-parse", "--abbrev-ref", "HEAD", chdir: self).chomp.presence
|
||||
end
|
||||
|
||||
def git_last_commit_date
|
||||
return unless git? && Utils.git_available?
|
||||
|
||||
Utils.popen_read("git", "show", "-s", "--format=%cd", "--date=short", "HEAD", chdir: self).chuzzle
|
||||
Utils.popen_read("git", "show", "-s", "--format=%cd", "--date=short", "HEAD", chdir: self).chomp.presence
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
# Contains backports from newer versions of Ruby
|
||||
require "backports/2.4.0/string/match"
|
||||
require "backports/2.5.0/string/delete_prefix"
|
||||
require "active_support/core_ext/object/blank"
|
||||
|
||||
class String
|
||||
# String.chomp, but if result is empty: returns nil instead.
|
||||
# Allows `chuzzle || foo` short-circuits.
|
||||
# TODO: Deprecate.
|
||||
def chuzzle
|
||||
s = chomp
|
||||
s unless s.empty?
|
||||
@ -12,6 +14,7 @@ class String
|
||||
end
|
||||
|
||||
class NilClass
|
||||
# TODO: Deprecate.
|
||||
def chuzzle; end
|
||||
end
|
||||
|
||||
|
||||
@ -417,7 +417,7 @@ class Formula
|
||||
def aliases
|
||||
@aliases ||= if tap
|
||||
tap.alias_reverse_table[full_name].to_a.map do |a|
|
||||
a.split("/")[-1]
|
||||
a.split("/").last
|
||||
end
|
||||
else
|
||||
[]
|
||||
@ -1371,7 +1371,7 @@ class Formula
|
||||
# an array of all {Formula} names
|
||||
# @private
|
||||
def self.names
|
||||
@names ||= (core_names + tap_names.map { |name| name.split("/")[-1] }).uniq.sort
|
||||
@names ||= (core_names + tap_names.map { |name| name.split("/").last }).uniq.sort
|
||||
end
|
||||
|
||||
# an array of all {Formula} files
|
||||
@ -1462,7 +1462,7 @@ class Formula
|
||||
# an array of all aliases
|
||||
# @private
|
||||
def self.aliases
|
||||
@aliases ||= (core_aliases + tap_aliases.map { |name| name.split("/")[-1] }).uniq.sort
|
||||
@aliases ||= (core_aliases + tap_aliases.map { |name| name.split("/").last }).uniq.sort
|
||||
end
|
||||
|
||||
# an array of all aliases, , which the tap formulae have the fully-qualified name
|
||||
|
||||
@ -537,7 +537,7 @@ class FormulaInstaller
|
||||
puts "All dependencies for #{formula.full_name} are satisfied."
|
||||
elsif !deps.empty?
|
||||
oh1 "Installing dependencies for #{formula.full_name}: " \
|
||||
"#{deps.map(&:first).map(&Formatter.method(:identifier)).join(", ")}",
|
||||
"#{deps.map(&:first).map(&Formatter.method(:identifier)).to_sentence}",
|
||||
truncate: false
|
||||
deps.each { |dep, options| install_dependency(dep, options) }
|
||||
end
|
||||
|
||||
@ -7,6 +7,21 @@ require "pp"
|
||||
|
||||
require_relative "load_path"
|
||||
|
||||
require "active_support/core_ext/object/blank"
|
||||
require "active_support/core_ext/numeric/time"
|
||||
require "active_support/core_ext/array/access"
|
||||
require "active_support/i18n"
|
||||
require "active_support/inflector/inflections"
|
||||
|
||||
I18n.backend.available_locales # Initialize locales so they can be overwritten.
|
||||
I18n.backend.store_translations :en, support: { array: { last_word_connector: " and " } }
|
||||
|
||||
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||
inflect.irregular "formula", "formulae"
|
||||
inflect.irregular "is", "are"
|
||||
inflect.irregular "it", "they"
|
||||
end
|
||||
|
||||
require "config"
|
||||
require "os"
|
||||
require "extend/ARGV"
|
||||
|
||||
@ -71,9 +71,7 @@ module Language
|
||||
rm_rf Dir[".cabal-sandbox/*packages.conf.d/"]
|
||||
end
|
||||
|
||||
def install_cabal_package(*args)
|
||||
options = args[-1].is_a?(Hash) ? args.pop : {}
|
||||
|
||||
def install_cabal_package(*args, **options)
|
||||
cabal_sandbox do
|
||||
cabal_install_tools(*options[:using]) if options[:using]
|
||||
|
||||
|
||||
@ -78,7 +78,7 @@ class Tab < OpenStruct
|
||||
end
|
||||
|
||||
if attributes["source"]["spec"].nil?
|
||||
version = PkgVersion.parse path.to_s.split("/")[-2]
|
||||
version = PkgVersion.parse path.to_s.split("/").second_to_last
|
||||
if version.head?
|
||||
attributes["source"]["spec"] = "head"
|
||||
else
|
||||
|
||||
@ -17,8 +17,8 @@ class Tap
|
||||
when 1
|
||||
user, repo = args.first.split("/", 2)
|
||||
when 2
|
||||
user = args[0]
|
||||
repo = args[1]
|
||||
user = args.first
|
||||
repo = args.second
|
||||
end
|
||||
|
||||
if [user, repo].any? { |part| part.nil? || part.include?("/") }
|
||||
@ -297,7 +297,7 @@ class Tap
|
||||
|
||||
link_completions_and_manpages
|
||||
|
||||
formatted_contents = Formatter.comma_and(*contents)&.prepend(" ")
|
||||
formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ")
|
||||
puts "Tapped#{formatted_contents} (#{path.abv})." unless quiet
|
||||
Descriptions.cache_formulae(formula_names)
|
||||
|
||||
@ -328,7 +328,7 @@ class Tap
|
||||
puts "Untapping #{name}..."
|
||||
|
||||
abv = path.abv
|
||||
formatted_contents = Formatter.comma_and(*contents)&.prepend(" ")
|
||||
formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ")
|
||||
|
||||
unpin if pinned?
|
||||
Descriptions.uncache_formulae(formula_names)
|
||||
@ -365,15 +365,15 @@ class Tap
|
||||
contents = []
|
||||
|
||||
if (command_count = command_files.count).positive?
|
||||
contents << Formatter.pluralize(command_count, "command")
|
||||
contents << "#{command_count} #{"command".pluralize(command_count)}"
|
||||
end
|
||||
|
||||
if (cask_count = cask_files.count).positive?
|
||||
contents << Formatter.pluralize(cask_count, "cask")
|
||||
contents << "#{cask_count} #{"cask".pluralize(cask_count)}"
|
||||
end
|
||||
|
||||
if (formula_count = formula_files.count).positive?
|
||||
contents << Formatter.pluralize(formula_count, "formula")
|
||||
contents << "#{formula_count} #{"formula".pluralize(formula_count)}"
|
||||
end
|
||||
|
||||
contents
|
||||
@ -722,7 +722,7 @@ class TapConfig
|
||||
return unless Utils.git_available?
|
||||
|
||||
tap.path.cd do
|
||||
Utils.popen_read("git", "config", "--local", "--get", "homebrew.#{key}").chuzzle
|
||||
Utils.popen_read("git", "config", "--local", "--get", "homebrew.#{key}").chomp.presence
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -419,10 +419,10 @@ describe Cask::DSL, :cask do
|
||||
let(:token) { "with-installer-script" }
|
||||
|
||||
it "allows installer script to be specified" do
|
||||
expect(cask.artifacts.to_a[0].path).to eq(Pathname("/usr/bin/true"))
|
||||
expect(cask.artifacts.to_a[0].args[:args]).to eq(["--flag"])
|
||||
expect(cask.artifacts.to_a[1].path).to eq(Pathname("/usr/bin/false"))
|
||||
expect(cask.artifacts.to_a[1].args[:args]).to eq(["--flag"])
|
||||
expect(cask.artifacts.to_a.first.path).to eq(Pathname("/usr/bin/true"))
|
||||
expect(cask.artifacts.to_a.first.args[:args]).to eq(["--flag"])
|
||||
expect(cask.artifacts.to_a.second.path).to eq(Pathname("/usr/bin/false"))
|
||||
expect(cask.artifacts.to_a.second.args[:args]).to eq(["--flag"])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ describe CleanupRefinement do
|
||||
end
|
||||
|
||||
it "returns true when path_modified_time < days_default" do
|
||||
allow_any_instance_of(Pathname).to receive(:mtime).and_return(Time.now - 2 * 60 * 60 * 24)
|
||||
allow_any_instance_of(Pathname).to receive(:mtime).and_return(2.days.ago)
|
||||
expect(path.prune?(1)).to be true
|
||||
end
|
||||
|
||||
@ -181,7 +181,7 @@ describe Homebrew::Cleanup do
|
||||
it "removes the download for the latest version after a week" do
|
||||
download = Cask::Cache.path/"#{cask.token}--#{cask.version}"
|
||||
|
||||
FileUtils.touch download, mtime: Time.now - 7 * 60 * 60 * 24
|
||||
FileUtils.touch download, mtime: 7.days.ago - 1.hour
|
||||
|
||||
subject.cleanup_cask(cask)
|
||||
|
||||
@ -203,13 +203,13 @@ describe Homebrew::Cleanup do
|
||||
end
|
||||
|
||||
it "cleans up logs if older than 14 days" do
|
||||
allow_any_instance_of(Pathname).to receive(:mtime).and_return(Time.now - 15 * 60 * 60 * 24)
|
||||
allow_any_instance_of(Pathname).to receive(:mtime).and_return(15.days.ago)
|
||||
subject.cleanup_logs
|
||||
expect(path).not_to exist
|
||||
end
|
||||
|
||||
it "does not clean up logs less than 14 days old" do
|
||||
allow_any_instance_of(Pathname).to receive(:mtime).and_return(Time.now - 2 * 60 * 60 * 24)
|
||||
allow_any_instance_of(Pathname).to receive(:mtime).and_return(2.days.ago)
|
||||
subject.cleanup_logs
|
||||
expect(path).to exist
|
||||
end
|
||||
|
||||
@ -53,45 +53,4 @@ describe Formatter do
|
||||
it { is_expected.to eq("\n") }
|
||||
end
|
||||
end
|
||||
|
||||
describe "::pluralize" do
|
||||
it "pluralizes words" do
|
||||
expect(described_class.pluralize(0, "cask")).to eq("0 casks")
|
||||
expect(described_class.pluralize(1, "cask")).to eq("1 cask")
|
||||
expect(described_class.pluralize(2, "cask")).to eq("2 casks")
|
||||
end
|
||||
|
||||
it "allows specifying custom plural forms" do
|
||||
expect(described_class.pluralize(1, "child", "children")).to eq("1 child")
|
||||
expect(described_class.pluralize(2, "child", "children")).to eq("2 children")
|
||||
end
|
||||
|
||||
it "has plural forms of Homebrew jargon" do
|
||||
expect(described_class.pluralize(1, "formula")).to eq("1 formula")
|
||||
expect(described_class.pluralize(2, "formula")).to eq("2 formulae")
|
||||
end
|
||||
|
||||
it "pluralizes the last word of a string" do
|
||||
expect(described_class.pluralize(1, "new formula")).to eq("1 new formula")
|
||||
expect(described_class.pluralize(2, "new formula")).to eq("2 new formulae")
|
||||
end
|
||||
end
|
||||
|
||||
describe "::comma_and" do
|
||||
it "returns nil if given no arguments" do
|
||||
expect(described_class.comma_and).to be nil
|
||||
end
|
||||
|
||||
it "returns the input as string if there is only one argument" do
|
||||
expect(described_class.comma_and(1)).to eq("1")
|
||||
end
|
||||
|
||||
it "concatenates two items with “and”" do
|
||||
expect(described_class.comma_and(1, 2)).to eq("1 and 2")
|
||||
end
|
||||
|
||||
it "concatenates all items with a comma and appends the last with “and”" do
|
||||
expect(described_class.comma_and(1, 2, 3)).to eq("1, 2 and 3")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -86,8 +86,8 @@ describe Patch do
|
||||
)
|
||||
|
||||
expect(patches.length).to eq(2)
|
||||
expect(patches[0].strip).to eq(:p1)
|
||||
expect(patches[1].strip).to eq(:p1)
|
||||
expect(patches.first.strip).to eq(:p1)
|
||||
expect(patches.second.strip).to eq(:p1)
|
||||
end
|
||||
|
||||
it "can create patches from a :p0 hash" do
|
||||
|
||||
@ -53,8 +53,9 @@ RSpec.shared_context "integration test" do
|
||||
def command_id_from_args(args)
|
||||
@command_count ||= 0
|
||||
pretty_args = args.join(" ").gsub(TEST_TMPDIR, "@TMPDIR@")
|
||||
file_and_line = caller[1].sub(/(.*\d+):.*/, '\1')
|
||||
.sub("#{HOMEBREW_LIBRARY_PATH}/test/", "")
|
||||
file_and_line = caller.second
|
||||
.sub(/(.*\d+):.*/, '\1')
|
||||
.sub("#{HOMEBREW_LIBRARY_PATH}/test/", "")
|
||||
"#{file_and_line}:brew #{pretty_args}:#{@command_count += 1}"
|
||||
end
|
||||
|
||||
|
||||
@ -152,13 +152,13 @@ def pretty_duration(s)
|
||||
if s > 59
|
||||
m = s / 60
|
||||
s %= 60
|
||||
res = Formatter.pluralize(m, "minute")
|
||||
res = "#{m} #{"minute".pluralize(m)}"
|
||||
return res if s.zero?
|
||||
|
||||
res << " "
|
||||
end
|
||||
|
||||
res << Formatter.pluralize(s, "second")
|
||||
res << "#{s} #{"second".pluralize(s)}"
|
||||
end
|
||||
|
||||
def interactive_shell(f = nil)
|
||||
|
||||
@ -54,7 +54,7 @@ module Utils
|
||||
end
|
||||
|
||||
def resolve_version(bottle_file)
|
||||
PkgVersion.parse receipt_path(bottle_file).split("/")[1]
|
||||
PkgVersion.parse receipt_path(bottle_file).split("/").second
|
||||
end
|
||||
|
||||
def formula_contents(bottle_file,
|
||||
|
||||
@ -98,28 +98,4 @@ module Formatter
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
def pluralize(count, singular, plural = nil, show_count: true)
|
||||
return (show_count ? "#{count} #{singular}" : singular.to_s) if count == 1
|
||||
|
||||
*adjectives, noun = singular.to_s.split(" ")
|
||||
|
||||
plural ||= {
|
||||
"formula" => "formulae",
|
||||
}.fetch(noun, "#{noun}s")
|
||||
|
||||
words = adjectives.push(plural).join(" ")
|
||||
|
||||
show_count ? "#{count} #{words}" : words
|
||||
end
|
||||
|
||||
def comma_and(*items)
|
||||
# TODO: Remove when RuboCop 0.57.3 is released.
|
||||
# False positive has been fixed and merged, but is not yet in a
|
||||
# stable release: https://github.com/rubocop-hq/rubocop/pull/6038
|
||||
*items, last = items.map(&:to_s) # rubocop:disable Lint/ShadowedArgument
|
||||
return last if items.empty?
|
||||
|
||||
"#{items.join(", ")} and #{last}"
|
||||
end
|
||||
end
|
||||
|
||||
@ -36,7 +36,7 @@ module Utils
|
||||
|
||||
@git_path ||= Utils.popen_read(
|
||||
HOMEBREW_SHIMS_PATH/"scm/git", "--homebrew=print-path"
|
||||
).chuzzle
|
||||
).chomp.presence
|
||||
end
|
||||
|
||||
def self.git_version
|
||||
|
||||
@ -7,8 +7,8 @@ module Tty
|
||||
|
||||
def width
|
||||
@width ||= begin
|
||||
width = `/bin/stty size 2>/dev/null`.split[1]
|
||||
width = `/usr/bin/tput cols 2>/dev/null`.split[0] if width.to_i.zero?
|
||||
_, width = `/bin/stty size 2>/dev/null`.split
|
||||
width, = `/usr/bin/tput cols 2>/dev/null`.split if width.to_i.zero?
|
||||
width ||= 80
|
||||
width.to_i
|
||||
end
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/array/wrap"
|
||||
require "active_support/core_ext/array/access"
|
||||
require "active_support/core_ext/array/conversions"
|
||||
require "active_support/core_ext/array/extract_options"
|
||||
require "active_support/core_ext/array/grouping"
|
||||
require "active_support/core_ext/array/prepend_and_append"
|
||||
require "active_support/core_ext/array/inquiry"
|
||||
@ -0,0 +1,92 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Array
|
||||
# Returns the tail of the array from +position+.
|
||||
#
|
||||
# %w( a b c d ).from(0) # => ["a", "b", "c", "d"]
|
||||
# %w( a b c d ).from(2) # => ["c", "d"]
|
||||
# %w( a b c d ).from(10) # => []
|
||||
# %w().from(0) # => []
|
||||
# %w( a b c d ).from(-2) # => ["c", "d"]
|
||||
# %w( a b c ).from(-10) # => []
|
||||
def from(position)
|
||||
self[position, length] || []
|
||||
end
|
||||
|
||||
# Returns the beginning of the array up to +position+.
|
||||
#
|
||||
# %w( a b c d ).to(0) # => ["a"]
|
||||
# %w( a b c d ).to(2) # => ["a", "b", "c"]
|
||||
# %w( a b c d ).to(10) # => ["a", "b", "c", "d"]
|
||||
# %w().to(0) # => []
|
||||
# %w( a b c d ).to(-2) # => ["a", "b", "c"]
|
||||
# %w( a b c ).to(-10) # => []
|
||||
def to(position)
|
||||
if position >= 0
|
||||
take position + 1
|
||||
else
|
||||
self[0..position]
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a copy of the Array without the specified elements.
|
||||
#
|
||||
# people = ["David", "Rafael", "Aaron", "Todd"]
|
||||
# people.without "Aaron", "Todd"
|
||||
# # => ["David", "Rafael"]
|
||||
#
|
||||
# Note: This is an optimization of <tt>Enumerable#without</tt> that uses <tt>Array#-</tt>
|
||||
# instead of <tt>Array#reject</tt> for performance reasons.
|
||||
def without(*elements)
|
||||
self - elements
|
||||
end
|
||||
|
||||
# Equal to <tt>self[1]</tt>.
|
||||
#
|
||||
# %w( a b c d e ).second # => "b"
|
||||
def second
|
||||
self[1]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[2]</tt>.
|
||||
#
|
||||
# %w( a b c d e ).third # => "c"
|
||||
def third
|
||||
self[2]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[3]</tt>.
|
||||
#
|
||||
# %w( a b c d e ).fourth # => "d"
|
||||
def fourth
|
||||
self[3]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[4]</tt>.
|
||||
#
|
||||
# %w( a b c d e ).fifth # => "e"
|
||||
def fifth
|
||||
self[4]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
|
||||
#
|
||||
# (1..42).to_a.forty_two # => 42
|
||||
def forty_two
|
||||
self[41]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[-3]</tt>.
|
||||
#
|
||||
# %w( a b c d e ).third_to_last # => "c"
|
||||
def third_to_last
|
||||
self[-3]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[-2]</tt>.
|
||||
#
|
||||
# %w( a b c d e ).second_to_last # => "d"
|
||||
def second_to_last
|
||||
self[-2]
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,213 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/xml_mini"
|
||||
require "active_support/core_ext/hash/keys"
|
||||
require "active_support/core_ext/string/inflections"
|
||||
require "active_support/core_ext/object/to_param"
|
||||
require "active_support/core_ext/object/to_query"
|
||||
|
||||
class Array
|
||||
# Converts the array to a comma-separated sentence where the last element is
|
||||
# joined by the connector word.
|
||||
#
|
||||
# You can pass the following options to change the default behavior. If you
|
||||
# pass an option key that doesn't exist in the list below, it will raise an
|
||||
# <tt>ArgumentError</tt>.
|
||||
#
|
||||
# ==== Options
|
||||
#
|
||||
# * <tt>:words_connector</tt> - The sign or word used to join the elements
|
||||
# in arrays with two or more elements (default: ", ").
|
||||
# * <tt>:two_words_connector</tt> - The sign or word used to join the elements
|
||||
# in arrays with two elements (default: " and ").
|
||||
# * <tt>:last_word_connector</tt> - The sign or word used to join the last element
|
||||
# in arrays with three or more elements (default: ", and ").
|
||||
# * <tt>:locale</tt> - If +i18n+ is available, you can set a locale and use
|
||||
# the connector options defined on the 'support.array' namespace in the
|
||||
# corresponding dictionary file.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# [].to_sentence # => ""
|
||||
# ['one'].to_sentence # => "one"
|
||||
# ['one', 'two'].to_sentence # => "one and two"
|
||||
# ['one', 'two', 'three'].to_sentence # => "one, two, and three"
|
||||
#
|
||||
# ['one', 'two'].to_sentence(passing: 'invalid option')
|
||||
# # => ArgumentError: Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale
|
||||
#
|
||||
# ['one', 'two'].to_sentence(two_words_connector: '-')
|
||||
# # => "one-two"
|
||||
#
|
||||
# ['one', 'two', 'three'].to_sentence(words_connector: ' or ', last_word_connector: ' or at least ')
|
||||
# # => "one or two or at least three"
|
||||
#
|
||||
# Using <tt>:locale</tt> option:
|
||||
#
|
||||
# # Given this locale dictionary:
|
||||
# #
|
||||
# # es:
|
||||
# # support:
|
||||
# # array:
|
||||
# # words_connector: " o "
|
||||
# # two_words_connector: " y "
|
||||
# # last_word_connector: " o al menos "
|
||||
#
|
||||
# ['uno', 'dos'].to_sentence(locale: :es)
|
||||
# # => "uno y dos"
|
||||
#
|
||||
# ['uno', 'dos', 'tres'].to_sentence(locale: :es)
|
||||
# # => "uno o dos o al menos tres"
|
||||
def to_sentence(options = {})
|
||||
options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
|
||||
|
||||
default_connectors = {
|
||||
words_connector: ", ",
|
||||
two_words_connector: " and ",
|
||||
last_word_connector: ", and "
|
||||
}
|
||||
if defined?(I18n)
|
||||
i18n_connectors = I18n.translate(:'support.array', locale: options[:locale], default: {})
|
||||
default_connectors.merge!(i18n_connectors)
|
||||
end
|
||||
options = default_connectors.merge!(options)
|
||||
|
||||
case length
|
||||
when 0
|
||||
""
|
||||
when 1
|
||||
"#{self[0]}"
|
||||
when 2
|
||||
"#{self[0]}#{options[:two_words_connector]}#{self[1]}"
|
||||
else
|
||||
"#{self[0...-1].join(options[:words_connector])}#{options[:last_word_connector]}#{self[-1]}"
|
||||
end
|
||||
end
|
||||
|
||||
# Extends <tt>Array#to_s</tt> to convert a collection of elements into a
|
||||
# comma separated id list if <tt>:db</tt> argument is given as the format.
|
||||
#
|
||||
# Blog.all.to_formatted_s(:db) # => "1,2,3"
|
||||
# Blog.none.to_formatted_s(:db) # => "null"
|
||||
# [1,2].to_formatted_s # => "[1, 2]"
|
||||
def to_formatted_s(format = :default)
|
||||
case format
|
||||
when :db
|
||||
if empty?
|
||||
"null"
|
||||
else
|
||||
collect(&:id).join(",")
|
||||
end
|
||||
else
|
||||
to_default_s
|
||||
end
|
||||
end
|
||||
alias_method :to_default_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
|
||||
# Returns a string that represents the array in XML by invoking +to_xml+
|
||||
# on each element. Active Record collections delegate their representation
|
||||
# in XML to this method.
|
||||
#
|
||||
# All elements are expected to respond to +to_xml+, if any of them does
|
||||
# not then an exception is raised.
|
||||
#
|
||||
# The root node reflects the class name of the first element in plural
|
||||
# if all elements belong to the same type and that's not Hash:
|
||||
#
|
||||
# customer.projects.to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <projects type="array">
|
||||
# <project>
|
||||
# <amount type="decimal">20000.0</amount>
|
||||
# <customer-id type="integer">1567</customer-id>
|
||||
# <deal-date type="date">2008-04-09</deal-date>
|
||||
# ...
|
||||
# </project>
|
||||
# <project>
|
||||
# <amount type="decimal">57230.0</amount>
|
||||
# <customer-id type="integer">1567</customer-id>
|
||||
# <deal-date type="date">2008-04-15</deal-date>
|
||||
# ...
|
||||
# </project>
|
||||
# </projects>
|
||||
#
|
||||
# Otherwise the root element is "objects":
|
||||
#
|
||||
# [{ foo: 1, bar: 2}, { baz: 3}].to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <objects type="array">
|
||||
# <object>
|
||||
# <bar type="integer">2</bar>
|
||||
# <foo type="integer">1</foo>
|
||||
# </object>
|
||||
# <object>
|
||||
# <baz type="integer">3</baz>
|
||||
# </object>
|
||||
# </objects>
|
||||
#
|
||||
# If the collection is empty the root element is "nil-classes" by default:
|
||||
#
|
||||
# [].to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <nil-classes type="array"/>
|
||||
#
|
||||
# To ensure a meaningful root element use the <tt>:root</tt> option:
|
||||
#
|
||||
# customer_with_no_projects.projects.to_xml(root: 'projects')
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <projects type="array"/>
|
||||
#
|
||||
# By default name of the node for the children of root is <tt>root.singularize</tt>.
|
||||
# You can change it with the <tt>:children</tt> option.
|
||||
#
|
||||
# The +options+ hash is passed downwards:
|
||||
#
|
||||
# Message.all.to_xml(skip_types: true)
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <messages>
|
||||
# <message>
|
||||
# <created-at>2008-03-07T09:58:18+01:00</created-at>
|
||||
# <id>1</id>
|
||||
# <name>1</name>
|
||||
# <updated-at>2008-03-07T09:58:18+01:00</updated-at>
|
||||
# <user-id>1</user-id>
|
||||
# </message>
|
||||
# </messages>
|
||||
#
|
||||
def to_xml(options = {})
|
||||
require "active_support/builder" unless defined?(Builder)
|
||||
|
||||
options = options.dup
|
||||
options[:indent] ||= 2
|
||||
options[:builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
|
||||
options[:root] ||= \
|
||||
if first.class != Hash && all? { |e| e.is_a?(first.class) }
|
||||
underscored = ActiveSupport::Inflector.underscore(first.class.name)
|
||||
ActiveSupport::Inflector.pluralize(underscored).tr("/", "_")
|
||||
else
|
||||
"objects"
|
||||
end
|
||||
|
||||
builder = options[:builder]
|
||||
builder.instruct! unless options.delete(:skip_instruct)
|
||||
|
||||
root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
|
||||
children = options.delete(:children) || root.singularize
|
||||
attributes = options[:skip_types] ? {} : { type: "array" }
|
||||
|
||||
if empty?
|
||||
builder.tag!(root, attributes)
|
||||
else
|
||||
builder.tag!(root, attributes) do
|
||||
each { |value| ActiveSupport::XmlMini.to_tag(children, value, options) }
|
||||
yield builder if block_given?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Hash
|
||||
# By default, only instances of Hash itself are extractable.
|
||||
# Subclasses of Hash may implement this method and return
|
||||
# true to declare themselves as extractable. If a Hash
|
||||
# is extractable, Array#extract_options! pops it from
|
||||
# the Array when it is the last element of the Array.
|
||||
def extractable_options?
|
||||
instance_of?(Hash)
|
||||
end
|
||||
end
|
||||
|
||||
class Array
|
||||
# Extracts options from a set of arguments. Removes and returns the last
|
||||
# element in the array if it's a hash, otherwise returns a blank hash.
|
||||
#
|
||||
# def options(*args)
|
||||
# args.extract_options!
|
||||
# end
|
||||
#
|
||||
# options(1, 2) # => {}
|
||||
# options(1, 2, a: :b) # => {:a=>:b}
|
||||
def extract_options!
|
||||
if last.is_a?(Hash) && last.extractable_options?
|
||||
pop
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,109 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Array
|
||||
# Splits or iterates over the array in groups of size +number+,
|
||||
# padding any remaining slots with +fill_with+ unless it is +false+.
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7 8 9 10).in_groups_of(3) {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5", "6"]
|
||||
# ["7", "8", "9"]
|
||||
# ["10", nil, nil]
|
||||
#
|
||||
# %w(1 2 3 4 5).in_groups_of(2, ' ') {|group| p group}
|
||||
# ["1", "2"]
|
||||
# ["3", "4"]
|
||||
# ["5", " "]
|
||||
#
|
||||
# %w(1 2 3 4 5).in_groups_of(2, false) {|group| p group}
|
||||
# ["1", "2"]
|
||||
# ["3", "4"]
|
||||
# ["5"]
|
||||
def in_groups_of(number, fill_with = nil)
|
||||
if number.to_i <= 0
|
||||
raise ArgumentError,
|
||||
"Group size must be a positive integer, was #{number.inspect}"
|
||||
end
|
||||
|
||||
if fill_with == false
|
||||
collection = self
|
||||
else
|
||||
# size % number gives how many extra we have;
|
||||
# subtracting from number gives how many to add;
|
||||
# modulo number ensures we don't add group of just fill.
|
||||
padding = (number - size % number) % number
|
||||
collection = dup.concat(Array.new(padding, fill_with))
|
||||
end
|
||||
|
||||
if block_given?
|
||||
collection.each_slice(number) { |slice| yield(slice) }
|
||||
else
|
||||
collection.each_slice(number).to_a
|
||||
end
|
||||
end
|
||||
|
||||
# Splits or iterates over the array in +number+ of groups, padding any
|
||||
# remaining slots with +fill_with+ unless it is +false+.
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group}
|
||||
# ["1", "2", "3", "4"]
|
||||
# ["5", "6", "7", nil]
|
||||
# ["8", "9", "10", nil]
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7 8 9 10).in_groups(3, ' ') {|group| p group}
|
||||
# ["1", "2", "3", "4"]
|
||||
# ["5", "6", "7", " "]
|
||||
# ["8", "9", "10", " "]
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups(3, false) {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5"]
|
||||
# ["6", "7"]
|
||||
def in_groups(number, fill_with = nil)
|
||||
# size.div number gives minor group size;
|
||||
# size % number gives how many objects need extra accommodation;
|
||||
# each group hold either division or division + 1 items.
|
||||
division = size.div number
|
||||
modulo = size % number
|
||||
|
||||
# create a new array avoiding dup
|
||||
groups = []
|
||||
start = 0
|
||||
|
||||
number.times do |index|
|
||||
length = division + (modulo > 0 && modulo > index ? 1 : 0)
|
||||
groups << last_group = slice(start, length)
|
||||
last_group << fill_with if fill_with != false &&
|
||||
modulo > 0 && length == division
|
||||
start += length
|
||||
end
|
||||
|
||||
if block_given?
|
||||
groups.each { |g| yield(g) }
|
||||
else
|
||||
groups
|
||||
end
|
||||
end
|
||||
|
||||
# Divides the array into one or more subarrays based on a delimiting +value+
|
||||
# or the result of an optional block.
|
||||
#
|
||||
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
|
||||
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
|
||||
def split(value = nil)
|
||||
arr = dup
|
||||
result = []
|
||||
if block_given?
|
||||
while (idx = arr.index { |i| yield i })
|
||||
result << arr.shift(idx)
|
||||
arr.shift
|
||||
end
|
||||
else
|
||||
while (idx = arr.index(value))
|
||||
result << arr.shift(idx)
|
||||
arr.shift
|
||||
end
|
||||
end
|
||||
result << arr
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/array_inquirer"
|
||||
|
||||
class Array
|
||||
# Wraps the array in an +ArrayInquirer+ object, which gives a friendlier way
|
||||
# to check its string-like contents.
|
||||
#
|
||||
# pets = [:cat, :dog].inquiry
|
||||
#
|
||||
# pets.cat? # => true
|
||||
# pets.ferret? # => false
|
||||
#
|
||||
# pets.any?(:cat, :ferret) # => true
|
||||
# pets.any?(:ferret, :alligator) # => false
|
||||
def inquiry
|
||||
ActiveSupport::ArrayInquirer.new(self)
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Array
|
||||
# The human way of thinking about adding stuff to the end of a list is with append.
|
||||
alias_method :append, :push unless [].respond_to?(:append)
|
||||
|
||||
# The human way of thinking about adding stuff to the beginning of a list is with prepend.
|
||||
alias_method :prepend, :unshift unless [].respond_to?(:prepend)
|
||||
end
|
||||
@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Array
|
||||
# Wraps its argument in an array unless it is already an array (or array-like).
|
||||
#
|
||||
# Specifically:
|
||||
#
|
||||
# * If the argument is +nil+ an empty array is returned.
|
||||
# * Otherwise, if the argument responds to +to_ary+ it is invoked, and its result returned.
|
||||
# * Otherwise, returns an array with the argument as its single element.
|
||||
#
|
||||
# Array.wrap(nil) # => []
|
||||
# Array.wrap([1, 2, 3]) # => [1, 2, 3]
|
||||
# Array.wrap(0) # => [0]
|
||||
#
|
||||
# This method is similar in purpose to <tt>Kernel#Array</tt>, but there are some differences:
|
||||
#
|
||||
# * If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt>
|
||||
# moves on to try +to_a+ if the returned value is +nil+, but <tt>Array.wrap</tt> returns
|
||||
# an array with the argument as its single element right away.
|
||||
# * If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt>
|
||||
# raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value.
|
||||
# * It does not call +to_a+ on the argument, if the argument does not respond to +to_ary+
|
||||
# it returns an array with the argument as its single element.
|
||||
#
|
||||
# The last point is easily explained with some enumerables:
|
||||
#
|
||||
# Array(foo: :bar) # => [[:foo, :bar]]
|
||||
# Array.wrap(foo: :bar) # => [{:foo=>:bar}]
|
||||
#
|
||||
# There's also a related idiom that uses the splat operator:
|
||||
#
|
||||
# [*object]
|
||||
#
|
||||
# which returns <tt>[]</tt> for +nil+, but calls to <tt>Array(object)</tt> otherwise.
|
||||
#
|
||||
# The differences with <tt>Kernel#Array</tt> explained above
|
||||
# apply to the rest of <tt>object</tt>s.
|
||||
def self.wrap(object)
|
||||
if object.nil?
|
||||
[]
|
||||
elsif object.respond_to?(:to_ary)
|
||||
object.to_ary || [object]
|
||||
else
|
||||
[object]
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "benchmark"
|
||||
|
||||
class << Benchmark
|
||||
# Benchmark realtime in milliseconds.
|
||||
#
|
||||
# Benchmark.realtime { User.all }
|
||||
# # => 8.0e-05
|
||||
#
|
||||
# Benchmark.ms { User.all }
|
||||
# # => 0.074
|
||||
def ms
|
||||
1000 * realtime { yield }
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/big_decimal/conversions"
|
||||
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "bigdecimal"
|
||||
require "bigdecimal/util"
|
||||
|
||||
module ActiveSupport
|
||||
module BigDecimalWithDefaultFormat #:nodoc:
|
||||
def to_s(format = "F")
|
||||
super(format)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
BigDecimal.prepend(ActiveSupport::BigDecimalWithDefaultFormat)
|
||||
@ -0,0 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/class/attribute"
|
||||
require "active_support/core_ext/class/subclasses"
|
||||
@ -0,0 +1,146 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/kernel/singleton_class"
|
||||
require "active_support/core_ext/module/redefine_method"
|
||||
require "active_support/core_ext/array/extract_options"
|
||||
|
||||
class Class
|
||||
# Declare a class-level attribute whose value is inheritable by subclasses.
|
||||
# Subclasses can change their own value and it will not impact parent class.
|
||||
#
|
||||
# ==== Options
|
||||
#
|
||||
# * <tt>:instance_reader</tt> - Sets the instance reader method (defaults to true).
|
||||
# * <tt>:instance_writer</tt> - Sets the instance writer method (defaults to true).
|
||||
# * <tt>:instance_accessor</tt> - Sets both instance methods (defaults to true).
|
||||
# * <tt>:instance_predicate</tt> - Sets a predicate method (defaults to true).
|
||||
# * <tt>:default</tt> - Sets a default value for the attribute (defaults to nil).
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# class Base
|
||||
# class_attribute :setting
|
||||
# end
|
||||
#
|
||||
# class Subclass < Base
|
||||
# end
|
||||
#
|
||||
# Base.setting = true
|
||||
# Subclass.setting # => true
|
||||
# Subclass.setting = false
|
||||
# Subclass.setting # => false
|
||||
# Base.setting # => true
|
||||
#
|
||||
# In the above case as long as Subclass does not assign a value to setting
|
||||
# by performing <tt>Subclass.setting = _something_</tt>, <tt>Subclass.setting</tt>
|
||||
# would read value assigned to parent class. Once Subclass assigns a value then
|
||||
# the value assigned by Subclass would be returned.
|
||||
#
|
||||
# This matches normal Ruby method inheritance: think of writing an attribute
|
||||
# on a subclass as overriding the reader method. However, you need to be aware
|
||||
# when using +class_attribute+ with mutable structures as +Array+ or +Hash+.
|
||||
# In such cases, you don't want to do changes in place. Instead use setters:
|
||||
#
|
||||
# Base.setting = []
|
||||
# Base.setting # => []
|
||||
# Subclass.setting # => []
|
||||
#
|
||||
# # Appending in child changes both parent and child because it is the same object:
|
||||
# Subclass.setting << :foo
|
||||
# Base.setting # => [:foo]
|
||||
# Subclass.setting # => [:foo]
|
||||
#
|
||||
# # Use setters to not propagate changes:
|
||||
# Base.setting = []
|
||||
# Subclass.setting += [:foo]
|
||||
# Base.setting # => []
|
||||
# Subclass.setting # => [:foo]
|
||||
#
|
||||
# For convenience, an instance predicate method is defined as well.
|
||||
# To skip it, pass <tt>instance_predicate: false</tt>.
|
||||
#
|
||||
# Subclass.setting? # => false
|
||||
#
|
||||
# Instances may overwrite the class value in the same way:
|
||||
#
|
||||
# Base.setting = true
|
||||
# object = Base.new
|
||||
# object.setting # => true
|
||||
# object.setting = false
|
||||
# object.setting # => false
|
||||
# Base.setting # => true
|
||||
#
|
||||
# To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
|
||||
#
|
||||
# object.setting # => NoMethodError
|
||||
# object.setting? # => NoMethodError
|
||||
#
|
||||
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
|
||||
#
|
||||
# object.setting = false # => NoMethodError
|
||||
#
|
||||
# To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
|
||||
#
|
||||
# To set a default value for the attribute, pass <tt>default:</tt>, like so:
|
||||
#
|
||||
# class_attribute :settings, default: {}
|
||||
def class_attribute(*attrs)
|
||||
options = attrs.extract_options!
|
||||
instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true)
|
||||
instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true)
|
||||
instance_predicate = options.fetch(:instance_predicate, true)
|
||||
default_value = options.fetch(:default, nil)
|
||||
|
||||
attrs.each do |name|
|
||||
singleton_class.silence_redefinition_of_method(name)
|
||||
define_singleton_method(name) { nil }
|
||||
|
||||
singleton_class.silence_redefinition_of_method("#{name}?")
|
||||
define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate
|
||||
|
||||
ivar = "@#{name}"
|
||||
|
||||
singleton_class.silence_redefinition_of_method("#{name}=")
|
||||
define_singleton_method("#{name}=") do |val|
|
||||
singleton_class.class_eval do
|
||||
redefine_method(name) { val }
|
||||
end
|
||||
|
||||
if singleton_class?
|
||||
class_eval do
|
||||
redefine_method(name) do
|
||||
if instance_variable_defined? ivar
|
||||
instance_variable_get ivar
|
||||
else
|
||||
singleton_class.send name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
val
|
||||
end
|
||||
|
||||
if instance_reader
|
||||
redefine_method(name) do
|
||||
if instance_variable_defined?(ivar)
|
||||
instance_variable_get ivar
|
||||
else
|
||||
self.class.public_send name
|
||||
end
|
||||
end
|
||||
|
||||
redefine_method("#{name}?") { !!public_send(name) } if instance_predicate
|
||||
end
|
||||
|
||||
if instance_writer
|
||||
redefine_method("#{name}=") do |val|
|
||||
instance_variable_set ivar, val
|
||||
end
|
||||
end
|
||||
|
||||
unless default_value.nil?
|
||||
self.send("#{name}=", default_value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# cattr_* became mattr_* aliases in 7dfbd91b0780fbd6a1dd9bfbc176e10894871d2d,
|
||||
# but we keep this around for libraries that directly require it knowing they
|
||||
# want cattr_*. No need to deprecate.
|
||||
require "active_support/core_ext/module/attribute_accessors"
|
||||
@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Class
|
||||
begin
|
||||
# Test if this Ruby supports each_object against singleton_class
|
||||
ObjectSpace.each_object(Numeric.singleton_class) {}
|
||||
|
||||
# Returns an array with all classes that are < than its receiver.
|
||||
#
|
||||
# class C; end
|
||||
# C.descendants # => []
|
||||
#
|
||||
# class B < C; end
|
||||
# C.descendants # => [B]
|
||||
#
|
||||
# class A < B; end
|
||||
# C.descendants # => [B, A]
|
||||
#
|
||||
# class D < C; end
|
||||
# C.descendants # => [B, A, D]
|
||||
def descendants
|
||||
descendants = []
|
||||
ObjectSpace.each_object(singleton_class) do |k|
|
||||
next if k.singleton_class?
|
||||
descendants.unshift k unless k == self
|
||||
end
|
||||
descendants
|
||||
end
|
||||
rescue StandardError # JRuby 9.0.4.0 and earlier
|
||||
def descendants
|
||||
descendants = []
|
||||
ObjectSpace.each_object(Class) do |k|
|
||||
descendants.unshift k if k < self
|
||||
end
|
||||
descendants.uniq!
|
||||
descendants
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an array with the direct children of +self+.
|
||||
#
|
||||
# class Foo; end
|
||||
# class Bar < Foo; end
|
||||
# class Baz < Bar; end
|
||||
#
|
||||
# Foo.subclasses # => [Bar]
|
||||
def subclasses
|
||||
subclasses, chain = [], descendants
|
||||
chain.each do |k|
|
||||
subclasses << k unless chain.any? { |c| c > k }
|
||||
end
|
||||
subclasses
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/date/acts_like"
|
||||
require "active_support/core_ext/date/blank"
|
||||
require "active_support/core_ext/date/calculations"
|
||||
require "active_support/core_ext/date/conversions"
|
||||
require "active_support/core_ext/date/zones"
|
||||
@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/object/acts_like"
|
||||
|
||||
class Date
|
||||
# Duck-types as a Date-like class. See Object#acts_like?.
|
||||
def acts_like_date?
|
||||
true
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "date"
|
||||
|
||||
class Date #:nodoc:
|
||||
# No Date is blank:
|
||||
#
|
||||
# Date.today.blank? # => false
|
||||
#
|
||||
# @return [false]
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,145 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "date"
|
||||
require "active_support/duration"
|
||||
require "active_support/core_ext/object/acts_like"
|
||||
require "active_support/core_ext/date/zones"
|
||||
require "active_support/core_ext/time/zones"
|
||||
require "active_support/core_ext/date_and_time/calculations"
|
||||
|
||||
class Date
|
||||
include DateAndTime::Calculations
|
||||
|
||||
class << self
|
||||
attr_accessor :beginning_of_week_default
|
||||
|
||||
# Returns the week start (e.g. :monday) for the current request, if this has been set (via Date.beginning_of_week=).
|
||||
# If <tt>Date.beginning_of_week</tt> has not been set for the current request, returns the week start specified in <tt>config.beginning_of_week</tt>.
|
||||
# If no config.beginning_of_week was specified, returns :monday.
|
||||
def beginning_of_week
|
||||
Thread.current[:beginning_of_week] || beginning_of_week_default || :monday
|
||||
end
|
||||
|
||||
# Sets <tt>Date.beginning_of_week</tt> to a week start (e.g. :monday) for current request/thread.
|
||||
#
|
||||
# This method accepts any of the following day symbols:
|
||||
# :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday
|
||||
def beginning_of_week=(week_start)
|
||||
Thread.current[:beginning_of_week] = find_beginning_of_week!(week_start)
|
||||
end
|
||||
|
||||
# Returns week start day symbol (e.g. :monday), or raises an +ArgumentError+ for invalid day symbol.
|
||||
def find_beginning_of_week!(week_start)
|
||||
raise ArgumentError, "Invalid beginning of week: #{week_start}" unless ::Date::DAYS_INTO_WEEK.key?(week_start)
|
||||
week_start
|
||||
end
|
||||
|
||||
# Returns a new Date representing the date 1 day ago (i.e. yesterday's date).
|
||||
def yesterday
|
||||
::Date.current.yesterday
|
||||
end
|
||||
|
||||
# Returns a new Date representing the date 1 day after today (i.e. tomorrow's date).
|
||||
def tomorrow
|
||||
::Date.current.tomorrow
|
||||
end
|
||||
|
||||
# Returns Time.zone.today when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns Date.today.
|
||||
def current
|
||||
::Time.zone ? ::Time.zone.today : ::Date.today
|
||||
end
|
||||
end
|
||||
|
||||
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
|
||||
# and then subtracts the specified number of seconds.
|
||||
def ago(seconds)
|
||||
in_time_zone.since(-seconds)
|
||||
end
|
||||
|
||||
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
|
||||
# and then adds the specified number of seconds
|
||||
def since(seconds)
|
||||
in_time_zone.since(seconds)
|
||||
end
|
||||
alias :in :since
|
||||
|
||||
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
|
||||
def beginning_of_day
|
||||
in_time_zone
|
||||
end
|
||||
alias :midnight :beginning_of_day
|
||||
alias :at_midnight :beginning_of_day
|
||||
alias :at_beginning_of_day :beginning_of_day
|
||||
|
||||
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the middle of the day (12:00)
|
||||
def middle_of_day
|
||||
in_time_zone.middle_of_day
|
||||
end
|
||||
alias :midday :middle_of_day
|
||||
alias :noon :middle_of_day
|
||||
alias :at_midday :middle_of_day
|
||||
alias :at_noon :middle_of_day
|
||||
alias :at_middle_of_day :middle_of_day
|
||||
|
||||
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the end of the day (23:59:59)
|
||||
def end_of_day
|
||||
in_time_zone.end_of_day
|
||||
end
|
||||
alias :at_end_of_day :end_of_day
|
||||
|
||||
def plus_with_duration(other) #:nodoc:
|
||||
if ActiveSupport::Duration === other
|
||||
other.since(self)
|
||||
else
|
||||
plus_without_duration(other)
|
||||
end
|
||||
end
|
||||
alias_method :plus_without_duration, :+
|
||||
alias_method :+, :plus_with_duration
|
||||
|
||||
def minus_with_duration(other) #:nodoc:
|
||||
if ActiveSupport::Duration === other
|
||||
plus_with_duration(-other)
|
||||
else
|
||||
minus_without_duration(other)
|
||||
end
|
||||
end
|
||||
alias_method :minus_without_duration, :-
|
||||
alias_method :-, :minus_with_duration
|
||||
|
||||
# Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with
|
||||
# any of these keys: <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>.
|
||||
def advance(options)
|
||||
options = options.dup
|
||||
d = self
|
||||
d = d >> options.delete(:years) * 12 if options[:years]
|
||||
d = d >> options.delete(:months) if options[:months]
|
||||
d = d + options.delete(:weeks) * 7 if options[:weeks]
|
||||
d = d + options.delete(:days) if options[:days]
|
||||
d
|
||||
end
|
||||
|
||||
# Returns a new Date where one or more of the elements have been changed according to the +options+ parameter.
|
||||
# The +options+ parameter is a hash with a combination of these keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>.
|
||||
#
|
||||
# Date.new(2007, 5, 12).change(day: 1) # => Date.new(2007, 5, 1)
|
||||
# Date.new(2007, 5, 12).change(year: 2005, month: 1) # => Date.new(2005, 1, 12)
|
||||
def change(options)
|
||||
::Date.new(
|
||||
options.fetch(:year, year),
|
||||
options.fetch(:month, month),
|
||||
options.fetch(:day, day)
|
||||
)
|
||||
end
|
||||
|
||||
# Allow Date to be compared with Time by converting to DateTime and relying on the <=> from there.
|
||||
def compare_with_coercion(other)
|
||||
if other.is_a?(Time)
|
||||
to_datetime <=> other
|
||||
else
|
||||
compare_without_coercion(other)
|
||||
end
|
||||
end
|
||||
alias_method :compare_without_coercion, :<=>
|
||||
alias_method :<=>, :compare_with_coercion
|
||||
end
|
||||
@ -0,0 +1,96 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "date"
|
||||
require "active_support/inflector/methods"
|
||||
require "active_support/core_ext/date/zones"
|
||||
require "active_support/core_ext/module/redefine_method"
|
||||
|
||||
class Date
|
||||
DATE_FORMATS = {
|
||||
short: "%d %b",
|
||||
long: "%B %d, %Y",
|
||||
db: "%Y-%m-%d",
|
||||
number: "%Y%m%d",
|
||||
long_ordinal: lambda { |date|
|
||||
day_format = ActiveSupport::Inflector.ordinalize(date.day)
|
||||
date.strftime("%B #{day_format}, %Y") # => "April 25th, 2007"
|
||||
},
|
||||
rfc822: "%d %b %Y",
|
||||
iso8601: lambda { |date| date.iso8601 }
|
||||
}
|
||||
|
||||
# Convert to a formatted string. See DATE_FORMATS for predefined formats.
|
||||
#
|
||||
# This method is aliased to <tt>to_s</tt>.
|
||||
#
|
||||
# date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007
|
||||
#
|
||||
# date.to_formatted_s(:db) # => "2007-11-10"
|
||||
# date.to_s(:db) # => "2007-11-10"
|
||||
#
|
||||
# date.to_formatted_s(:short) # => "10 Nov"
|
||||
# date.to_formatted_s(:number) # => "20071110"
|
||||
# date.to_formatted_s(:long) # => "November 10, 2007"
|
||||
# date.to_formatted_s(:long_ordinal) # => "November 10th, 2007"
|
||||
# date.to_formatted_s(:rfc822) # => "10 Nov 2007"
|
||||
# date.to_formatted_s(:iso8601) # => "2007-11-10"
|
||||
#
|
||||
# == Adding your own date formats to to_formatted_s
|
||||
# You can add your own formats to the Date::DATE_FORMATS hash.
|
||||
# Use the format name as the hash key and either a strftime string
|
||||
# or Proc instance that takes a date argument as the value.
|
||||
#
|
||||
# # config/initializers/date_formats.rb
|
||||
# Date::DATE_FORMATS[:month_and_year] = '%B %Y'
|
||||
# Date::DATE_FORMATS[:short_ordinal] = ->(date) { date.strftime("%B #{date.day.ordinalize}") }
|
||||
def to_formatted_s(format = :default)
|
||||
if formatter = DATE_FORMATS[format]
|
||||
if formatter.respond_to?(:call)
|
||||
formatter.call(self).to_s
|
||||
else
|
||||
strftime(formatter)
|
||||
end
|
||||
else
|
||||
to_default_s
|
||||
end
|
||||
end
|
||||
alias_method :to_default_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
|
||||
# Overrides the default inspect method with a human readable one, e.g., "Mon, 21 Feb 2005"
|
||||
def readable_inspect
|
||||
strftime("%a, %d %b %Y")
|
||||
end
|
||||
alias_method :default_inspect, :inspect
|
||||
alias_method :inspect, :readable_inspect
|
||||
|
||||
silence_redefinition_of_method :to_time
|
||||
|
||||
# Converts a Date instance to a Time, where the time is set to the beginning of the day.
|
||||
# The timezone can be either :local or :utc (default :local).
|
||||
#
|
||||
# date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007
|
||||
#
|
||||
# date.to_time # => 2007-11-10 00:00:00 0800
|
||||
# date.to_time(:local) # => 2007-11-10 00:00:00 0800
|
||||
#
|
||||
# date.to_time(:utc) # => 2007-11-10 00:00:00 UTC
|
||||
#
|
||||
# NOTE: The :local timezone is Ruby's *process* timezone, i.e. ENV['TZ'].
|
||||
# If the *application's* timezone is needed, then use +in_time_zone+ instead.
|
||||
def to_time(form = :local)
|
||||
raise ArgumentError, "Expected :local or :utc, got #{form.inspect}." unless [:local, :utc].include?(form)
|
||||
::Time.send(form, year, month, day)
|
||||
end
|
||||
|
||||
silence_redefinition_of_method :xmlschema
|
||||
|
||||
# Returns a string which represents the time in used time zone as DateTime
|
||||
# defined by XML Schema:
|
||||
#
|
||||
# date = Date.new(2015, 05, 23) # => Sat, 23 May 2015
|
||||
# date.xmlschema # => "2015-05-23T00:00:00+04:00"
|
||||
def xmlschema
|
||||
in_time_zone.xmlschema
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "date"
|
||||
require "active_support/core_ext/date_and_time/zones"
|
||||
|
||||
class Date
|
||||
include DateAndTime::Zones
|
||||
end
|
||||
@ -0,0 +1,374 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/object/try"
|
||||
|
||||
module DateAndTime
|
||||
module Calculations
|
||||
DAYS_INTO_WEEK = {
|
||||
monday: 0,
|
||||
tuesday: 1,
|
||||
wednesday: 2,
|
||||
thursday: 3,
|
||||
friday: 4,
|
||||
saturday: 5,
|
||||
sunday: 6
|
||||
}
|
||||
WEEKEND_DAYS = [ 6, 0 ]
|
||||
|
||||
# Returns a new date/time representing yesterday.
|
||||
def yesterday
|
||||
advance(days: -1)
|
||||
end
|
||||
|
||||
# Returns a new date/time the specified number of days ago.
|
||||
def prev_day(days = 1)
|
||||
advance(days: -days)
|
||||
end
|
||||
|
||||
# Returns a new date/time representing tomorrow.
|
||||
def tomorrow
|
||||
advance(days: 1)
|
||||
end
|
||||
|
||||
# Returns a new date/time the specified number of days in the future.
|
||||
def next_day(days = 1)
|
||||
advance(days: days)
|
||||
end
|
||||
|
||||
# Returns true if the date/time is today.
|
||||
def today?
|
||||
to_date == ::Date.current
|
||||
end
|
||||
|
||||
# Returns true if the date/time is in the past.
|
||||
def past?
|
||||
self < self.class.current
|
||||
end
|
||||
|
||||
# Returns true if the date/time is in the future.
|
||||
def future?
|
||||
self > self.class.current
|
||||
end
|
||||
|
||||
# Returns true if the date/time falls on a Saturday or Sunday.
|
||||
def on_weekend?
|
||||
WEEKEND_DAYS.include?(wday)
|
||||
end
|
||||
|
||||
# Returns true if the date/time does not fall on a Saturday or Sunday.
|
||||
def on_weekday?
|
||||
!WEEKEND_DAYS.include?(wday)
|
||||
end
|
||||
|
||||
# Returns a new date/time the specified number of days ago.
|
||||
def days_ago(days)
|
||||
advance(days: -days)
|
||||
end
|
||||
|
||||
# Returns a new date/time the specified number of days in the future.
|
||||
def days_since(days)
|
||||
advance(days: days)
|
||||
end
|
||||
|
||||
# Returns a new date/time the specified number of weeks ago.
|
||||
def weeks_ago(weeks)
|
||||
advance(weeks: -weeks)
|
||||
end
|
||||
|
||||
# Returns a new date/time the specified number of weeks in the future.
|
||||
def weeks_since(weeks)
|
||||
advance(weeks: weeks)
|
||||
end
|
||||
|
||||
# Returns a new date/time the specified number of months ago.
|
||||
def months_ago(months)
|
||||
advance(months: -months)
|
||||
end
|
||||
|
||||
# Returns a new date/time the specified number of months in the future.
|
||||
def months_since(months)
|
||||
advance(months: months)
|
||||
end
|
||||
|
||||
# Returns a new date/time the specified number of years ago.
|
||||
def years_ago(years)
|
||||
advance(years: -years)
|
||||
end
|
||||
|
||||
# Returns a new date/time the specified number of years in the future.
|
||||
def years_since(years)
|
||||
advance(years: years)
|
||||
end
|
||||
|
||||
# Returns a new date/time at the start of the month.
|
||||
#
|
||||
# today = Date.today # => Thu, 18 Jun 2015
|
||||
# today.beginning_of_month # => Mon, 01 Jun 2015
|
||||
#
|
||||
# +DateTime+ objects will have a time set to 0:00.
|
||||
#
|
||||
# now = DateTime.current # => Thu, 18 Jun 2015 15:23:13 +0000
|
||||
# now.beginning_of_month # => Mon, 01 Jun 2015 00:00:00 +0000
|
||||
def beginning_of_month
|
||||
first_hour(change(day: 1))
|
||||
end
|
||||
alias :at_beginning_of_month :beginning_of_month
|
||||
|
||||
# Returns a new date/time at the start of the quarter.
|
||||
#
|
||||
# today = Date.today # => Fri, 10 Jul 2015
|
||||
# today.beginning_of_quarter # => Wed, 01 Jul 2015
|
||||
#
|
||||
# +DateTime+ objects will have a time set to 0:00.
|
||||
#
|
||||
# now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
|
||||
# now.beginning_of_quarter # => Wed, 01 Jul 2015 00:00:00 +0000
|
||||
def beginning_of_quarter
|
||||
first_quarter_month = [10, 7, 4, 1].detect { |m| m <= month }
|
||||
beginning_of_month.change(month: first_quarter_month)
|
||||
end
|
||||
alias :at_beginning_of_quarter :beginning_of_quarter
|
||||
|
||||
# Returns a new date/time at the end of the quarter.
|
||||
#
|
||||
# today = Date.today # => Fri, 10 Jul 2015
|
||||
# today.end_of_quarter # => Wed, 30 Sep 2015
|
||||
#
|
||||
# +DateTime+ objects will have a time set to 23:59:59.
|
||||
#
|
||||
# now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
|
||||
# now.end_of_quarter # => Wed, 30 Sep 2015 23:59:59 +0000
|
||||
def end_of_quarter
|
||||
last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month }
|
||||
beginning_of_month.change(month: last_quarter_month).end_of_month
|
||||
end
|
||||
alias :at_end_of_quarter :end_of_quarter
|
||||
|
||||
# Returns a new date/time at the beginning of the year.
|
||||
#
|
||||
# today = Date.today # => Fri, 10 Jul 2015
|
||||
# today.beginning_of_year # => Thu, 01 Jan 2015
|
||||
#
|
||||
# +DateTime+ objects will have a time set to 0:00.
|
||||
#
|
||||
# now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
|
||||
# now.beginning_of_year # => Thu, 01 Jan 2015 00:00:00 +0000
|
||||
def beginning_of_year
|
||||
change(month: 1).beginning_of_month
|
||||
end
|
||||
alias :at_beginning_of_year :beginning_of_year
|
||||
|
||||
# Returns a new date/time representing the given day in the next week.
|
||||
#
|
||||
# today = Date.today # => Thu, 07 May 2015
|
||||
# today.next_week # => Mon, 11 May 2015
|
||||
#
|
||||
# The +given_day_in_next_week+ defaults to the beginning of the week
|
||||
# which is determined by +Date.beginning_of_week+ or +config.beginning_of_week+
|
||||
# when set.
|
||||
#
|
||||
# today = Date.today # => Thu, 07 May 2015
|
||||
# today.next_week(:friday) # => Fri, 15 May 2015
|
||||
#
|
||||
# +DateTime+ objects have their time set to 0:00 unless +same_time+ is true.
|
||||
#
|
||||
# now = DateTime.current # => Thu, 07 May 2015 13:31:16 +0000
|
||||
# now.next_week # => Mon, 11 May 2015 00:00:00 +0000
|
||||
def next_week(given_day_in_next_week = Date.beginning_of_week, same_time: false)
|
||||
result = first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week)))
|
||||
same_time ? copy_time_to(result) : result
|
||||
end
|
||||
|
||||
# Returns a new date/time representing the next weekday.
|
||||
def next_weekday
|
||||
if next_day.on_weekend?
|
||||
next_week(:monday, same_time: true)
|
||||
else
|
||||
next_day
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a new date/time the specified number of months in the future.
|
||||
def next_month(months = 1)
|
||||
advance(months: months)
|
||||
end
|
||||
|
||||
# Short-hand for months_since(3)
|
||||
def next_quarter
|
||||
months_since(3)
|
||||
end
|
||||
|
||||
# Returns a new date/time the specified number of years in the future.
|
||||
def next_year(years = 1)
|
||||
advance(years: years)
|
||||
end
|
||||
|
||||
# Returns a new date/time representing the given day in the previous week.
|
||||
# Week is assumed to start on +start_day+, default is
|
||||
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
|
||||
# DateTime objects have their time set to 0:00 unless +same_time+ is true.
|
||||
def prev_week(start_day = Date.beginning_of_week, same_time: false)
|
||||
result = first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day)))
|
||||
same_time ? copy_time_to(result) : result
|
||||
end
|
||||
alias_method :last_week, :prev_week
|
||||
|
||||
# Returns a new date/time representing the previous weekday.
|
||||
def prev_weekday
|
||||
if prev_day.on_weekend?
|
||||
copy_time_to(beginning_of_week(:friday))
|
||||
else
|
||||
prev_day
|
||||
end
|
||||
end
|
||||
alias_method :last_weekday, :prev_weekday
|
||||
|
||||
# Returns a new date/time the specified number of months ago.
|
||||
def prev_month(months = 1)
|
||||
advance(months: -months)
|
||||
end
|
||||
|
||||
# Short-hand for months_ago(1).
|
||||
def last_month
|
||||
months_ago(1)
|
||||
end
|
||||
|
||||
# Short-hand for months_ago(3).
|
||||
def prev_quarter
|
||||
months_ago(3)
|
||||
end
|
||||
alias_method :last_quarter, :prev_quarter
|
||||
|
||||
# Returns a new date/time the specified number of years ago.
|
||||
def prev_year(years = 1)
|
||||
advance(years: -years)
|
||||
end
|
||||
|
||||
# Short-hand for years_ago(1).
|
||||
def last_year
|
||||
years_ago(1)
|
||||
end
|
||||
|
||||
# Returns the number of days to the start of the week on the given day.
|
||||
# Week is assumed to start on +start_day+, default is
|
||||
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
|
||||
def days_to_week_start(start_day = Date.beginning_of_week)
|
||||
start_day_number = DAYS_INTO_WEEK[start_day]
|
||||
current_day_number = wday != 0 ? wday - 1 : 6
|
||||
(current_day_number - start_day_number) % 7
|
||||
end
|
||||
|
||||
# Returns a new date/time representing the start of this week on the given day.
|
||||
# Week is assumed to start on +start_day+, default is
|
||||
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
|
||||
# +DateTime+ objects have their time set to 0:00.
|
||||
def beginning_of_week(start_day = Date.beginning_of_week)
|
||||
result = days_ago(days_to_week_start(start_day))
|
||||
acts_like?(:time) ? result.midnight : result
|
||||
end
|
||||
alias :at_beginning_of_week :beginning_of_week
|
||||
|
||||
# Returns Monday of this week assuming that week starts on Monday.
|
||||
# +DateTime+ objects have their time set to 0:00.
|
||||
def monday
|
||||
beginning_of_week(:monday)
|
||||
end
|
||||
|
||||
# Returns a new date/time representing the end of this week on the given day.
|
||||
# Week is assumed to start on +start_day+, default is
|
||||
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
|
||||
# DateTime objects have their time set to 23:59:59.
|
||||
def end_of_week(start_day = Date.beginning_of_week)
|
||||
last_hour(days_since(6 - days_to_week_start(start_day)))
|
||||
end
|
||||
alias :at_end_of_week :end_of_week
|
||||
|
||||
# Returns Sunday of this week assuming that week starts on Monday.
|
||||
# +DateTime+ objects have their time set to 23:59:59.
|
||||
def sunday
|
||||
end_of_week(:monday)
|
||||
end
|
||||
|
||||
# Returns a new date/time representing the end of the month.
|
||||
# DateTime objects will have a time set to 23:59:59.
|
||||
def end_of_month
|
||||
last_day = ::Time.days_in_month(month, year)
|
||||
last_hour(days_since(last_day - day))
|
||||
end
|
||||
alias :at_end_of_month :end_of_month
|
||||
|
||||
# Returns a new date/time representing the end of the year.
|
||||
# DateTime objects will have a time set to 23:59:59.
|
||||
def end_of_year
|
||||
change(month: 12).end_of_month
|
||||
end
|
||||
alias :at_end_of_year :end_of_year
|
||||
|
||||
# Returns a Range representing the whole day of the current date/time.
|
||||
def all_day
|
||||
beginning_of_day..end_of_day
|
||||
end
|
||||
|
||||
# Returns a Range representing the whole week of the current date/time.
|
||||
# Week starts on start_day, default is <tt>Date.beginning_of_week</tt> or <tt>config.beginning_of_week</tt> when set.
|
||||
def all_week(start_day = Date.beginning_of_week)
|
||||
beginning_of_week(start_day)..end_of_week(start_day)
|
||||
end
|
||||
|
||||
# Returns a Range representing the whole month of the current date/time.
|
||||
def all_month
|
||||
beginning_of_month..end_of_month
|
||||
end
|
||||
|
||||
# Returns a Range representing the whole quarter of the current date/time.
|
||||
def all_quarter
|
||||
beginning_of_quarter..end_of_quarter
|
||||
end
|
||||
|
||||
# Returns a Range representing the whole year of the current date/time.
|
||||
def all_year
|
||||
beginning_of_year..end_of_year
|
||||
end
|
||||
|
||||
# Returns a new date/time representing the next occurrence of the specified day of week.
|
||||
#
|
||||
# today = Date.today # => Thu, 14 Dec 2017
|
||||
# today.next_occurring(:monday) # => Mon, 18 Dec 2017
|
||||
# today.next_occurring(:thursday) # => Thu, 21 Dec 2017
|
||||
def next_occurring(day_of_week)
|
||||
current_day_number = wday != 0 ? wday - 1 : 6
|
||||
from_now = DAYS_INTO_WEEK.fetch(day_of_week) - current_day_number
|
||||
from_now += 7 unless from_now > 0
|
||||
advance(days: from_now)
|
||||
end
|
||||
|
||||
# Returns a new date/time representing the previous occurrence of the specified day of week.
|
||||
#
|
||||
# today = Date.today # => Thu, 14 Dec 2017
|
||||
# today.prev_occurring(:monday) # => Mon, 11 Dec 2017
|
||||
# today.prev_occurring(:thursday) # => Thu, 07 Dec 2017
|
||||
def prev_occurring(day_of_week)
|
||||
current_day_number = wday != 0 ? wday - 1 : 6
|
||||
ago = current_day_number - DAYS_INTO_WEEK.fetch(day_of_week)
|
||||
ago += 7 unless ago > 0
|
||||
advance(days: -ago)
|
||||
end
|
||||
|
||||
private
|
||||
def first_hour(date_or_time)
|
||||
date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time
|
||||
end
|
||||
|
||||
def last_hour(date_or_time)
|
||||
date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time
|
||||
end
|
||||
|
||||
def days_span(day)
|
||||
(DAYS_INTO_WEEK[day] - DAYS_INTO_WEEK[Date.beginning_of_week]) % 7
|
||||
end
|
||||
|
||||
def copy_time_to(other)
|
||||
other.change(hour: hour, min: min, sec: sec, nsec: try(:nsec))
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/module/attribute_accessors"
|
||||
|
||||
module DateAndTime
|
||||
module Compatibility
|
||||
# If true, +to_time+ preserves the timezone offset of receiver.
|
||||
#
|
||||
# NOTE: With Ruby 2.4+ the default for +to_time+ changed from
|
||||
# converting to the local system time, to preserving the offset
|
||||
# of the receiver. For backwards compatibility we're overriding
|
||||
# this behavior, but new apps will have an initializer that sets
|
||||
# this to true, because the new behavior is preferred.
|
||||
mattr_accessor :preserve_timezone, instance_writer: false, default: false
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DateAndTime
|
||||
module Zones
|
||||
# Returns the simultaneous time in <tt>Time.zone</tt> if a zone is given or
|
||||
# if Time.zone_default is set. Otherwise, it returns the current time.
|
||||
#
|
||||
# Time.zone = 'Hawaii' # => 'Hawaii'
|
||||
# Time.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00
|
||||
# Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00
|
||||
#
|
||||
# This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone
|
||||
# instead of the operating system's time zone.
|
||||
#
|
||||
# You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument,
|
||||
# and the conversion will be based on that zone instead of <tt>Time.zone</tt>.
|
||||
#
|
||||
# Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
|
||||
# Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00
|
||||
def in_time_zone(zone = ::Time.zone)
|
||||
time_zone = ::Time.find_zone! zone
|
||||
time = acts_like?(:time) ? self : nil
|
||||
|
||||
if time_zone
|
||||
time_with_zone(time, time_zone)
|
||||
else
|
||||
time || to_time
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def time_with_zone(time, zone)
|
||||
if time
|
||||
ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone)
|
||||
else
|
||||
ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/date_time/acts_like"
|
||||
require "active_support/core_ext/date_time/blank"
|
||||
require "active_support/core_ext/date_time/calculations"
|
||||
require "active_support/core_ext/date_time/compatibility"
|
||||
require "active_support/core_ext/date_time/conversions"
|
||||
@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "date"
|
||||
require "active_support/core_ext/object/acts_like"
|
||||
|
||||
class DateTime
|
||||
# Duck-types as a Date-like class. See Object#acts_like?.
|
||||
def acts_like_date?
|
||||
true
|
||||
end
|
||||
|
||||
# Duck-types as a Time-like class. See Object#acts_like?.
|
||||
def acts_like_time?
|
||||
true
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "date"
|
||||
|
||||
class DateTime #:nodoc:
|
||||
# No DateTime is ever blank:
|
||||
#
|
||||
# DateTime.now.blank? # => false
|
||||
#
|
||||
# @return [false]
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,211 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "date"
|
||||
|
||||
class DateTime
|
||||
class << self
|
||||
# Returns <tt>Time.zone.now.to_datetime</tt> when <tt>Time.zone</tt> or
|
||||
# <tt>config.time_zone</tt> are set, otherwise returns
|
||||
# <tt>Time.now.to_datetime</tt>.
|
||||
def current
|
||||
::Time.zone ? ::Time.zone.now.to_datetime : ::Time.now.to_datetime
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the number of seconds since 00:00:00.
|
||||
#
|
||||
# DateTime.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0
|
||||
# DateTime.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296
|
||||
# DateTime.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399
|
||||
def seconds_since_midnight
|
||||
sec + (min * 60) + (hour * 3600)
|
||||
end
|
||||
|
||||
# Returns the number of seconds until 23:59:59.
|
||||
#
|
||||
# DateTime.new(2012, 8, 29, 0, 0, 0).seconds_until_end_of_day # => 86399
|
||||
# DateTime.new(2012, 8, 29, 12, 34, 56).seconds_until_end_of_day # => 41103
|
||||
# DateTime.new(2012, 8, 29, 23, 59, 59).seconds_until_end_of_day # => 0
|
||||
def seconds_until_end_of_day
|
||||
end_of_day.to_i - to_i
|
||||
end
|
||||
|
||||
# Returns the fraction of a second as a +Rational+
|
||||
#
|
||||
# DateTime.new(2012, 8, 29, 0, 0, 0.5).subsec # => (1/2)
|
||||
def subsec
|
||||
sec_fraction
|
||||
end
|
||||
|
||||
# Returns a new DateTime where one or more of the elements have been changed
|
||||
# according to the +options+ parameter. The time options (<tt>:hour</tt>,
|
||||
# <tt>:min</tt>, <tt>:sec</tt>) reset cascadingly, so if only the hour is
|
||||
# passed, then minute and sec is set to 0. If the hour and minute is passed,
|
||||
# then sec is set to 0. The +options+ parameter takes a hash with any of these
|
||||
# keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>,
|
||||
# <tt>:min</tt>, <tt>:sec</tt>, <tt>:offset</tt>, <tt>:start</tt>.
|
||||
#
|
||||
# DateTime.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => DateTime.new(2012, 8, 1, 22, 35, 0)
|
||||
# DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => DateTime.new(1981, 8, 1, 22, 35, 0)
|
||||
# DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => DateTime.new(1981, 8, 29, 0, 0, 0)
|
||||
def change(options)
|
||||
if new_nsec = options[:nsec]
|
||||
raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec]
|
||||
new_fraction = Rational(new_nsec, 1000000000)
|
||||
else
|
||||
new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
|
||||
new_fraction = Rational(new_usec, 1000000)
|
||||
end
|
||||
|
||||
raise ArgumentError, "argument out of range" if new_fraction >= 1
|
||||
|
||||
::DateTime.civil(
|
||||
options.fetch(:year, year),
|
||||
options.fetch(:month, month),
|
||||
options.fetch(:day, day),
|
||||
options.fetch(:hour, hour),
|
||||
options.fetch(:min, options[:hour] ? 0 : min),
|
||||
options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) + new_fraction,
|
||||
options.fetch(:offset, offset),
|
||||
options.fetch(:start, start)
|
||||
)
|
||||
end
|
||||
|
||||
# Uses Date to provide precise Time calculations for years, months, and days.
|
||||
# The +options+ parameter takes a hash with any of these keys: <tt>:years</tt>,
|
||||
# <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>,
|
||||
# <tt>:minutes</tt>, <tt>:seconds</tt>.
|
||||
def advance(options)
|
||||
unless options[:weeks].nil?
|
||||
options[:weeks], partial_weeks = options[:weeks].divmod(1)
|
||||
options[:days] = options.fetch(:days, 0) + 7 * partial_weeks
|
||||
end
|
||||
|
||||
unless options[:days].nil?
|
||||
options[:days], partial_days = options[:days].divmod(1)
|
||||
options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
|
||||
end
|
||||
|
||||
d = to_date.advance(options)
|
||||
datetime_advanced_by_date = change(year: d.year, month: d.month, day: d.day)
|
||||
seconds_to_advance = \
|
||||
options.fetch(:seconds, 0) +
|
||||
options.fetch(:minutes, 0) * 60 +
|
||||
options.fetch(:hours, 0) * 3600
|
||||
|
||||
if seconds_to_advance.zero?
|
||||
datetime_advanced_by_date
|
||||
else
|
||||
datetime_advanced_by_date.since(seconds_to_advance)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a new DateTime representing the time a number of seconds ago.
|
||||
# Do not use this method in combination with x.months, use months_ago instead!
|
||||
def ago(seconds)
|
||||
since(-seconds)
|
||||
end
|
||||
|
||||
# Returns a new DateTime representing the time a number of seconds since the
|
||||
# instance time. Do not use this method in combination with x.months, use
|
||||
# months_since instead!
|
||||
def since(seconds)
|
||||
self + Rational(seconds.round, 86400)
|
||||
end
|
||||
alias :in :since
|
||||
|
||||
# Returns a new DateTime representing the start of the day (0:00).
|
||||
def beginning_of_day
|
||||
change(hour: 0)
|
||||
end
|
||||
alias :midnight :beginning_of_day
|
||||
alias :at_midnight :beginning_of_day
|
||||
alias :at_beginning_of_day :beginning_of_day
|
||||
|
||||
# Returns a new DateTime representing the middle of the day (12:00)
|
||||
def middle_of_day
|
||||
change(hour: 12)
|
||||
end
|
||||
alias :midday :middle_of_day
|
||||
alias :noon :middle_of_day
|
||||
alias :at_midday :middle_of_day
|
||||
alias :at_noon :middle_of_day
|
||||
alias :at_middle_of_day :middle_of_day
|
||||
|
||||
# Returns a new DateTime representing the end of the day (23:59:59).
|
||||
def end_of_day
|
||||
change(hour: 23, min: 59, sec: 59, usec: Rational(999999999, 1000))
|
||||
end
|
||||
alias :at_end_of_day :end_of_day
|
||||
|
||||
# Returns a new DateTime representing the start of the hour (hh:00:00).
|
||||
def beginning_of_hour
|
||||
change(min: 0)
|
||||
end
|
||||
alias :at_beginning_of_hour :beginning_of_hour
|
||||
|
||||
# Returns a new DateTime representing the end of the hour (hh:59:59).
|
||||
def end_of_hour
|
||||
change(min: 59, sec: 59, usec: Rational(999999999, 1000))
|
||||
end
|
||||
alias :at_end_of_hour :end_of_hour
|
||||
|
||||
# Returns a new DateTime representing the start of the minute (hh:mm:00).
|
||||
def beginning_of_minute
|
||||
change(sec: 0)
|
||||
end
|
||||
alias :at_beginning_of_minute :beginning_of_minute
|
||||
|
||||
# Returns a new DateTime representing the end of the minute (hh:mm:59).
|
||||
def end_of_minute
|
||||
change(sec: 59, usec: Rational(999999999, 1000))
|
||||
end
|
||||
alias :at_end_of_minute :end_of_minute
|
||||
|
||||
# Returns a <tt>Time</tt> instance of the simultaneous time in the system timezone.
|
||||
def localtime(utc_offset = nil)
|
||||
utc = new_offset(0)
|
||||
|
||||
Time.utc(
|
||||
utc.year, utc.month, utc.day,
|
||||
utc.hour, utc.min, utc.sec + utc.sec_fraction
|
||||
).getlocal(utc_offset)
|
||||
end
|
||||
alias_method :getlocal, :localtime
|
||||
|
||||
# Returns a <tt>Time</tt> instance of the simultaneous time in the UTC timezone.
|
||||
#
|
||||
# DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)) # => Mon, 21 Feb 2005 10:11:12 -0600
|
||||
# DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc # => Mon, 21 Feb 2005 16:11:12 UTC
|
||||
def utc
|
||||
utc = new_offset(0)
|
||||
|
||||
Time.utc(
|
||||
utc.year, utc.month, utc.day,
|
||||
utc.hour, utc.min, utc.sec + utc.sec_fraction
|
||||
)
|
||||
end
|
||||
alias_method :getgm, :utc
|
||||
alias_method :getutc, :utc
|
||||
alias_method :gmtime, :utc
|
||||
|
||||
# Returns +true+ if <tt>offset == 0</tt>.
|
||||
def utc?
|
||||
offset == 0
|
||||
end
|
||||
|
||||
# Returns the offset value in seconds.
|
||||
def utc_offset
|
||||
(offset * 86400).to_i
|
||||
end
|
||||
|
||||
# Layers additional behavior on DateTime#<=> so that Time and
|
||||
# ActiveSupport::TimeWithZone instances can be compared with a DateTime.
|
||||
def <=>(other)
|
||||
if other.respond_to? :to_datetime
|
||||
super other.to_datetime rescue nil
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/date_and_time/compatibility"
|
||||
require "active_support/core_ext/module/redefine_method"
|
||||
|
||||
class DateTime
|
||||
include DateAndTime::Compatibility
|
||||
|
||||
silence_redefinition_of_method :to_time
|
||||
|
||||
# Either return an instance of +Time+ with the same UTC offset
|
||||
# as +self+ or an instance of +Time+ representing the same time
|
||||
# in the local system timezone depending on the setting of
|
||||
# on the setting of +ActiveSupport.to_time_preserves_timezone+.
|
||||
def to_time
|
||||
preserve_timezone ? getlocal(utc_offset) : getlocal
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,107 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "date"
|
||||
require "active_support/inflector/methods"
|
||||
require "active_support/core_ext/time/conversions"
|
||||
require "active_support/core_ext/date_time/calculations"
|
||||
require "active_support/values/time_zone"
|
||||
|
||||
class DateTime
|
||||
# Convert to a formatted string. See Time::DATE_FORMATS for predefined formats.
|
||||
#
|
||||
# This method is aliased to <tt>to_s</tt>.
|
||||
#
|
||||
# === Examples
|
||||
# datetime = DateTime.civil(2007, 12, 4, 0, 0, 0, 0) # => Tue, 04 Dec 2007 00:00:00 +0000
|
||||
#
|
||||
# datetime.to_formatted_s(:db) # => "2007-12-04 00:00:00"
|
||||
# datetime.to_s(:db) # => "2007-12-04 00:00:00"
|
||||
# datetime.to_s(:number) # => "20071204000000"
|
||||
# datetime.to_formatted_s(:short) # => "04 Dec 00:00"
|
||||
# datetime.to_formatted_s(:long) # => "December 04, 2007 00:00"
|
||||
# datetime.to_formatted_s(:long_ordinal) # => "December 4th, 2007 00:00"
|
||||
# datetime.to_formatted_s(:rfc822) # => "Tue, 04 Dec 2007 00:00:00 +0000"
|
||||
# datetime.to_formatted_s(:iso8601) # => "2007-12-04T00:00:00+00:00"
|
||||
#
|
||||
# == Adding your own datetime formats to to_formatted_s
|
||||
# DateTime formats are shared with Time. You can add your own to the
|
||||
# Time::DATE_FORMATS hash. Use the format name as the hash key and
|
||||
# either a strftime string or Proc instance that takes a time or
|
||||
# datetime argument as the value.
|
||||
#
|
||||
# # config/initializers/time_formats.rb
|
||||
# Time::DATE_FORMATS[:month_and_year] = '%B %Y'
|
||||
# Time::DATE_FORMATS[:short_ordinal] = lambda { |time| time.strftime("%B #{time.day.ordinalize}") }
|
||||
def to_formatted_s(format = :default)
|
||||
if formatter = ::Time::DATE_FORMATS[format]
|
||||
formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
|
||||
else
|
||||
to_default_s
|
||||
end
|
||||
end
|
||||
alias_method :to_default_s, :to_s if instance_methods(false).include?(:to_s)
|
||||
alias_method :to_s, :to_formatted_s
|
||||
|
||||
# Returns a formatted string of the offset from UTC, or an alternative
|
||||
# string if the time zone is already UTC.
|
||||
#
|
||||
# datetime = DateTime.civil(2000, 1, 1, 0, 0, 0, Rational(-6, 24))
|
||||
# datetime.formatted_offset # => "-06:00"
|
||||
# datetime.formatted_offset(false) # => "-0600"
|
||||
def formatted_offset(colon = true, alternate_utc_string = nil)
|
||||
utc? && alternate_utc_string || ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon)
|
||||
end
|
||||
|
||||
# Overrides the default inspect method with a human readable one, e.g., "Mon, 21 Feb 2005 14:30:00 +0000".
|
||||
def readable_inspect
|
||||
to_s(:rfc822)
|
||||
end
|
||||
alias_method :default_inspect, :inspect
|
||||
alias_method :inspect, :readable_inspect
|
||||
|
||||
# Returns DateTime with local offset for given year if format is local else
|
||||
# offset is zero.
|
||||
#
|
||||
# DateTime.civil_from_format :local, 2012
|
||||
# # => Sun, 01 Jan 2012 00:00:00 +0300
|
||||
# DateTime.civil_from_format :local, 2012, 12, 17
|
||||
# # => Mon, 17 Dec 2012 00:00:00 +0000
|
||||
def self.civil_from_format(utc_or_local, year, month = 1, day = 1, hour = 0, min = 0, sec = 0)
|
||||
if utc_or_local.to_sym == :local
|
||||
offset = ::Time.local(year, month, day).utc_offset.to_r / 86400
|
||||
else
|
||||
offset = 0
|
||||
end
|
||||
civil(year, month, day, hour, min, sec, offset)
|
||||
end
|
||||
|
||||
# Converts +self+ to a floating-point number of seconds, including fractional microseconds, since the Unix epoch.
|
||||
def to_f
|
||||
seconds_since_unix_epoch.to_f + sec_fraction
|
||||
end
|
||||
|
||||
# Converts +self+ to an integer number of seconds since the Unix epoch.
|
||||
def to_i
|
||||
seconds_since_unix_epoch.to_i
|
||||
end
|
||||
|
||||
# Returns the fraction of a second as microseconds
|
||||
def usec
|
||||
(sec_fraction * 1_000_000).to_i
|
||||
end
|
||||
|
||||
# Returns the fraction of a second as nanoseconds
|
||||
def nsec
|
||||
(sec_fraction * 1_000_000_000).to_i
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def offset_in_seconds
|
||||
(offset * 86400).to_i
|
||||
end
|
||||
|
||||
def seconds_since_unix_epoch
|
||||
(jd - 2440588) * 86400 - offset_in_seconds + seconds_since_midnight
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "securerandom"
|
||||
|
||||
module Digest
|
||||
module UUID
|
||||
DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
|
||||
URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
|
||||
OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
|
||||
X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
|
||||
|
||||
# Generates a v5 non-random UUID (Universally Unique IDentifier).
|
||||
#
|
||||
# Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs.
|
||||
# uuid_from_hash always generates the same UUID for a given name and namespace combination.
|
||||
#
|
||||
# See RFC 4122 for details of UUID at: https://www.ietf.org/rfc/rfc4122.txt
|
||||
def self.uuid_from_hash(hash_class, uuid_namespace, name)
|
||||
if hash_class == Digest::MD5
|
||||
version = 3
|
||||
elsif hash_class == Digest::SHA1
|
||||
version = 5
|
||||
else
|
||||
raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}."
|
||||
end
|
||||
|
||||
hash = hash_class.new
|
||||
hash.update(uuid_namespace)
|
||||
hash.update(name)
|
||||
|
||||
ary = hash.digest.unpack("NnnnnN")
|
||||
ary[2] = (ary[2] & 0x0FFF) | (version << 12)
|
||||
ary[3] = (ary[3] & 0x3FFF) | 0x8000
|
||||
|
||||
"%08x-%04x-%04x-%04x-%04x%08x" % ary
|
||||
end
|
||||
|
||||
# Convenience method for uuid_from_hash using Digest::MD5.
|
||||
def self.uuid_v3(uuid_namespace, name)
|
||||
uuid_from_hash(Digest::MD5, uuid_namespace, name)
|
||||
end
|
||||
|
||||
# Convenience method for uuid_from_hash using Digest::SHA1.
|
||||
def self.uuid_v5(uuid_namespace, name)
|
||||
uuid_from_hash(Digest::SHA1, uuid_namespace, name)
|
||||
end
|
||||
|
||||
# Convenience method for SecureRandom.uuid.
|
||||
def self.uuid_v4
|
||||
SecureRandom.uuid
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,164 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Enumerable
|
||||
# Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements
|
||||
# when we omit an identity.
|
||||
#
|
||||
# We tried shimming it to attempt the fast native method, rescue TypeError,
|
||||
# and fall back to the compatible implementation, but that's much slower than
|
||||
# just calling the compat method in the first place.
|
||||
if Enumerable.instance_methods(false).include?(:sum) && !((?a..?b).sum rescue false)
|
||||
# :stopdoc:
|
||||
|
||||
# We can't use Refinements here because Refinements with Module which will be prepended
|
||||
# doesn't work well https://bugs.ruby-lang.org/issues/13446
|
||||
alias :_original_sum_with_required_identity :sum
|
||||
private :_original_sum_with_required_identity
|
||||
|
||||
# :startdoc:
|
||||
|
||||
# Calculates a sum from the elements.
|
||||
#
|
||||
# payments.sum { |p| p.price * p.tax_rate }
|
||||
# payments.sum(&:price)
|
||||
#
|
||||
# The latter is a shortcut for:
|
||||
#
|
||||
# payments.inject(0) { |sum, p| sum + p.price }
|
||||
#
|
||||
# It can also calculate the sum without the use of a block.
|
||||
#
|
||||
# [5, 15, 10].sum # => 30
|
||||
# ['foo', 'bar'].sum # => "foobar"
|
||||
# [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5]
|
||||
#
|
||||
# The default sum of an empty list is zero. You can override this default:
|
||||
#
|
||||
# [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0)
|
||||
def sum(identity = nil, &block)
|
||||
if identity
|
||||
_original_sum_with_required_identity(identity, &block)
|
||||
elsif block_given?
|
||||
map(&block).sum(identity)
|
||||
else
|
||||
inject(:+) || 0
|
||||
end
|
||||
end
|
||||
else
|
||||
def sum(identity = nil, &block)
|
||||
if block_given?
|
||||
map(&block).sum(identity)
|
||||
else
|
||||
sum = identity ? inject(identity, :+) : inject(:+)
|
||||
sum || identity || 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Convert an enumerable to a hash.
|
||||
#
|
||||
# people.index_by(&:login)
|
||||
# # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...}
|
||||
# people.index_by { |person| "#{person.first_name} #{person.last_name}" }
|
||||
# # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...}
|
||||
def index_by
|
||||
if block_given?
|
||||
result = {}
|
||||
each { |elem| result[yield(elem)] = elem }
|
||||
result
|
||||
else
|
||||
to_enum(:index_by) { size if respond_to?(:size) }
|
||||
end
|
||||
end
|
||||
|
||||
# Returns +true+ if the enumerable has more than 1 element. Functionally
|
||||
# equivalent to <tt>enum.to_a.size > 1</tt>. Can be called with a block too,
|
||||
# much like any?, so <tt>people.many? { |p| p.age > 26 }</tt> returns +true+
|
||||
# if more than one person is over 26.
|
||||
def many?
|
||||
cnt = 0
|
||||
if block_given?
|
||||
any? do |element|
|
||||
cnt += 1 if yield element
|
||||
cnt > 1
|
||||
end
|
||||
else
|
||||
any? { (cnt += 1) > 1 }
|
||||
end
|
||||
end
|
||||
|
||||
# The negative of the <tt>Enumerable#include?</tt>. Returns +true+ if the
|
||||
# collection does not include the object.
|
||||
def exclude?(object)
|
||||
!include?(object)
|
||||
end
|
||||
|
||||
# Returns a copy of the enumerable without the specified elements.
|
||||
#
|
||||
# ["David", "Rafael", "Aaron", "Todd"].without "Aaron", "Todd"
|
||||
# # => ["David", "Rafael"]
|
||||
#
|
||||
# {foo: 1, bar: 2, baz: 3}.without :bar
|
||||
# # => {foo: 1, baz: 3}
|
||||
def without(*elements)
|
||||
reject { |element| elements.include?(element) }
|
||||
end
|
||||
|
||||
# Convert an enumerable to an array based on the given key.
|
||||
#
|
||||
# [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name)
|
||||
# # => ["David", "Rafael", "Aaron"]
|
||||
#
|
||||
# [{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pluck(:id, :name)
|
||||
# # => [[1, "David"], [2, "Rafael"]]
|
||||
def pluck(*keys)
|
||||
if keys.many?
|
||||
map { |element| keys.map { |key| element[key] } }
|
||||
else
|
||||
map { |element| element[keys.first] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Range #:nodoc:
|
||||
# Optimize range sum to use arithmetic progression if a block is not given and
|
||||
# we have a range of numeric values.
|
||||
def sum(identity = nil)
|
||||
if block_given? || !(first.is_a?(Integer) && last.is_a?(Integer))
|
||||
super
|
||||
else
|
||||
actual_last = exclude_end? ? (last - 1) : last
|
||||
if actual_last >= first
|
||||
sum = identity || 0
|
||||
sum + (actual_last - first + 1) * (actual_last + first) / 2
|
||||
else
|
||||
identity || 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Array#sum was added in Ruby 2.4 but it only works with Numeric elements.
|
||||
#
|
||||
# We tried shimming it to attempt the fast native method, rescue TypeError,
|
||||
# and fall back to the compatible implementation, but that's much slower than
|
||||
# just calling the compat method in the first place.
|
||||
if Array.instance_methods(false).include?(:sum) && !(%w[a].sum rescue false)
|
||||
# Using Refinements here in order not to expose our internal method
|
||||
using Module.new {
|
||||
refine Array do
|
||||
alias :orig_sum :sum
|
||||
end
|
||||
}
|
||||
|
||||
class Array
|
||||
def sum(init = nil, &block) #:nodoc:
|
||||
if init.is_a?(Numeric) || first.is_a?(Numeric)
|
||||
init ||= 0
|
||||
orig_sum(init, &block)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/file/atomic"
|
||||
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/hash/compact"
|
||||
require "active_support/core_ext/hash/conversions"
|
||||
require "active_support/core_ext/hash/deep_merge"
|
||||
require "active_support/core_ext/hash/except"
|
||||
require "active_support/core_ext/hash/indifferent_access"
|
||||
require "active_support/core_ext/hash/keys"
|
||||
require "active_support/core_ext/hash/reverse_merge"
|
||||
require "active_support/core_ext/hash/slice"
|
||||
require "active_support/core_ext/hash/transform_values"
|
||||
@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Hash
|
||||
unless Hash.instance_methods(false).include?(:compact)
|
||||
# Returns a hash with non +nil+ values.
|
||||
#
|
||||
# hash = { a: true, b: false, c: nil }
|
||||
# hash.compact # => { a: true, b: false }
|
||||
# hash # => { a: true, b: false, c: nil }
|
||||
# { c: nil }.compact # => {}
|
||||
# { c: true }.compact # => { c: true }
|
||||
def compact
|
||||
select { |_, value| !value.nil? }
|
||||
end
|
||||
end
|
||||
|
||||
unless Hash.instance_methods(false).include?(:compact!)
|
||||
# Replaces current hash with non +nil+ values.
|
||||
# Returns +nil+ if no changes were made, otherwise returns the hash.
|
||||
#
|
||||
# hash = { a: true, b: false, c: nil }
|
||||
# hash.compact! # => { a: true, b: false }
|
||||
# hash # => { a: true, b: false }
|
||||
# { c: true }.compact! # => nil
|
||||
def compact!
|
||||
reject! { |_, value| value.nil? }
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,263 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/xml_mini"
|
||||
require "active_support/time"
|
||||
require "active_support/core_ext/object/blank"
|
||||
require "active_support/core_ext/object/to_param"
|
||||
require "active_support/core_ext/object/to_query"
|
||||
require "active_support/core_ext/array/wrap"
|
||||
require "active_support/core_ext/hash/reverse_merge"
|
||||
require "active_support/core_ext/string/inflections"
|
||||
|
||||
class Hash
|
||||
# Returns a string containing an XML representation of its receiver:
|
||||
#
|
||||
# { foo: 1, bar: 2 }.to_xml
|
||||
# # =>
|
||||
# # <?xml version="1.0" encoding="UTF-8"?>
|
||||
# # <hash>
|
||||
# # <foo type="integer">1</foo>
|
||||
# # <bar type="integer">2</bar>
|
||||
# # </hash>
|
||||
#
|
||||
# To do so, the method loops over the pairs and builds nodes that depend on
|
||||
# the _values_. Given a pair +key+, +value+:
|
||||
#
|
||||
# * If +value+ is a hash there's a recursive call with +key+ as <tt>:root</tt>.
|
||||
#
|
||||
# * If +value+ is an array there's a recursive call with +key+ as <tt>:root</tt>,
|
||||
# and +key+ singularized as <tt>:children</tt>.
|
||||
#
|
||||
# * If +value+ is a callable object it must expect one or two arguments. Depending
|
||||
# on the arity, the callable is invoked with the +options+ hash as first argument
|
||||
# with +key+ as <tt>:root</tt>, and +key+ singularized as second argument. The
|
||||
# callable can add nodes by using <tt>options[:builder]</tt>.
|
||||
#
|
||||
# {foo: lambda { |options, key| options[:builder].b(key) }}.to_xml
|
||||
# # => "<b>foo</b>"
|
||||
#
|
||||
# * If +value+ responds to +to_xml+ the method is invoked with +key+ as <tt>:root</tt>.
|
||||
#
|
||||
# class Foo
|
||||
# def to_xml(options)
|
||||
# options[:builder].bar 'fooing!'
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# { foo: Foo.new }.to_xml(skip_instruct: true)
|
||||
# # =>
|
||||
# # <hash>
|
||||
# # <bar>fooing!</bar>
|
||||
# # </hash>
|
||||
#
|
||||
# * Otherwise, a node with +key+ as tag is created with a string representation of
|
||||
# +value+ as text node. If +value+ is +nil+ an attribute "nil" set to "true" is added.
|
||||
# Unless the option <tt>:skip_types</tt> exists and is true, an attribute "type" is
|
||||
# added as well according to the following mapping:
|
||||
#
|
||||
# XML_TYPE_NAMES = {
|
||||
# "Symbol" => "symbol",
|
||||
# "Integer" => "integer",
|
||||
# "BigDecimal" => "decimal",
|
||||
# "Float" => "float",
|
||||
# "TrueClass" => "boolean",
|
||||
# "FalseClass" => "boolean",
|
||||
# "Date" => "date",
|
||||
# "DateTime" => "dateTime",
|
||||
# "Time" => "dateTime"
|
||||
# }
|
||||
#
|
||||
# By default the root node is "hash", but that's configurable via the <tt>:root</tt> option.
|
||||
#
|
||||
# The default XML builder is a fresh instance of <tt>Builder::XmlMarkup</tt>. You can
|
||||
# configure your own builder with the <tt>:builder</tt> option. The method also accepts
|
||||
# options like <tt>:dasherize</tt> and friends, they are forwarded to the builder.
|
||||
def to_xml(options = {})
|
||||
require "active_support/builder" unless defined?(Builder)
|
||||
|
||||
options = options.dup
|
||||
options[:indent] ||= 2
|
||||
options[:root] ||= "hash"
|
||||
options[:builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
|
||||
|
||||
builder = options[:builder]
|
||||
builder.instruct! unless options.delete(:skip_instruct)
|
||||
|
||||
root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
|
||||
|
||||
builder.tag!(root) do
|
||||
each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options) }
|
||||
yield builder if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
# Returns a Hash containing a collection of pairs when the key is the node name and the value is
|
||||
# its content
|
||||
#
|
||||
# xml = <<-XML
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <hash>
|
||||
# <foo type="integer">1</foo>
|
||||
# <bar type="integer">2</bar>
|
||||
# </hash>
|
||||
# XML
|
||||
#
|
||||
# hash = Hash.from_xml(xml)
|
||||
# # => {"hash"=>{"foo"=>1, "bar"=>2}}
|
||||
#
|
||||
# +DisallowedType+ is raised if the XML contains attributes with <tt>type="yaml"</tt> or
|
||||
# <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to
|
||||
# parse this XML.
|
||||
#
|
||||
# Custom +disallowed_types+ can also be passed in the form of an
|
||||
# array.
|
||||
#
|
||||
# xml = <<-XML
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <hash>
|
||||
# <foo type="integer">1</foo>
|
||||
# <bar type="string">"David"</bar>
|
||||
# </hash>
|
||||
# XML
|
||||
#
|
||||
# hash = Hash.from_xml(xml, ['integer'])
|
||||
# # => ActiveSupport::XMLConverter::DisallowedType: Disallowed type attribute: "integer"
|
||||
#
|
||||
# Note that passing custom disallowed types will override the default types,
|
||||
# which are Symbol and YAML.
|
||||
def from_xml(xml, disallowed_types = nil)
|
||||
ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h
|
||||
end
|
||||
|
||||
# Builds a Hash from XML just like <tt>Hash.from_xml</tt>, but also allows Symbol and YAML.
|
||||
def from_trusted_xml(xml)
|
||||
from_xml xml, []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ActiveSupport
|
||||
class XMLConverter # :nodoc:
|
||||
# Raised if the XML contains attributes with type="yaml" or
|
||||
# type="symbol". Read Hash#from_xml for more details.
|
||||
class DisallowedType < StandardError
|
||||
def initialize(type)
|
||||
super "Disallowed type attribute: #{type.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
DISALLOWED_TYPES = %w(symbol yaml)
|
||||
|
||||
def initialize(xml, disallowed_types = nil)
|
||||
@xml = normalize_keys(XmlMini.parse(xml))
|
||||
@disallowed_types = disallowed_types || DISALLOWED_TYPES
|
||||
end
|
||||
|
||||
def to_h
|
||||
deep_to_h(@xml)
|
||||
end
|
||||
|
||||
private
|
||||
def normalize_keys(params)
|
||||
case params
|
||||
when Hash
|
||||
Hash[params.map { |k, v| [k.to_s.tr("-", "_"), normalize_keys(v)] } ]
|
||||
when Array
|
||||
params.map { |v| normalize_keys(v) }
|
||||
else
|
||||
params
|
||||
end
|
||||
end
|
||||
|
||||
def deep_to_h(value)
|
||||
case value
|
||||
when Hash
|
||||
process_hash(value)
|
||||
when Array
|
||||
process_array(value)
|
||||
when String
|
||||
value
|
||||
else
|
||||
raise "can't typecast #{value.class.name} - #{value.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def process_hash(value)
|
||||
if value.include?("type") && !value["type"].is_a?(Hash) && @disallowed_types.include?(value["type"])
|
||||
raise DisallowedType, value["type"]
|
||||
end
|
||||
|
||||
if become_array?(value)
|
||||
_, entries = Array.wrap(value.detect { |k, v| not v.is_a?(String) })
|
||||
if entries.nil? || value["__content__"].try(:empty?)
|
||||
[]
|
||||
else
|
||||
case entries
|
||||
when Array
|
||||
entries.collect { |v| deep_to_h(v) }
|
||||
when Hash
|
||||
[deep_to_h(entries)]
|
||||
else
|
||||
raise "can't typecast #{entries.inspect}"
|
||||
end
|
||||
end
|
||||
elsif become_content?(value)
|
||||
process_content(value)
|
||||
|
||||
elsif become_empty_string?(value)
|
||||
""
|
||||
elsif become_hash?(value)
|
||||
xml_value = Hash[value.map { |k, v| [k, deep_to_h(v)] }]
|
||||
|
||||
# Turn { files: { file: #<StringIO> } } into { files: #<StringIO> } so it is compatible with
|
||||
# how multipart uploaded files from HTML appear
|
||||
xml_value["file"].is_a?(StringIO) ? xml_value["file"] : xml_value
|
||||
end
|
||||
end
|
||||
|
||||
def become_content?(value)
|
||||
value["type"] == "file" || (value["__content__"] && (value.keys.size == 1 || value["__content__"].present?))
|
||||
end
|
||||
|
||||
def become_array?(value)
|
||||
value["type"] == "array"
|
||||
end
|
||||
|
||||
def become_empty_string?(value)
|
||||
# { "string" => true }
|
||||
# No tests fail when the second term is removed.
|
||||
value["type"] == "string" && value["nil"] != "true"
|
||||
end
|
||||
|
||||
def become_hash?(value)
|
||||
!nothing?(value) && !garbage?(value)
|
||||
end
|
||||
|
||||
def nothing?(value)
|
||||
# blank or nil parsed values are represented by nil
|
||||
value.blank? || value["nil"] == "true"
|
||||
end
|
||||
|
||||
def garbage?(value)
|
||||
# If the type is the only element which makes it then
|
||||
# this still makes the value nil, except if type is
|
||||
# an XML node(where type['value'] is a Hash)
|
||||
value["type"] && !value["type"].is_a?(::Hash) && value.size == 1
|
||||
end
|
||||
|
||||
def process_content(value)
|
||||
content = value["__content__"]
|
||||
if parser = ActiveSupport::XmlMini::PARSING[value["type"]]
|
||||
parser.arity == 1 ? parser.call(content) : parser.call(content, value)
|
||||
else
|
||||
content
|
||||
end
|
||||
end
|
||||
|
||||
def process_array(value)
|
||||
value.map! { |i| deep_to_h(i) }
|
||||
value.length > 1 ? value : value.first
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Hash
|
||||
# Returns a hash that includes everything except given keys.
|
||||
# hash = { a: true, b: false, c: nil }
|
||||
# hash.except(:c) # => { a: true, b: false }
|
||||
# hash.except(:a, :b) # => { c: nil }
|
||||
# hash # => { a: true, b: false, c: nil }
|
||||
#
|
||||
# This is useful for limiting a set of parameters to everything but a few known toggles:
|
||||
# @person.update(params[:person].except(:admin))
|
||||
def except(*keys)
|
||||
dup.except!(*keys)
|
||||
end
|
||||
|
||||
# Removes the given keys from hash and returns it.
|
||||
# hash = { a: true, b: false, c: nil }
|
||||
# hash.except!(:c) # => { a: true, b: false }
|
||||
# hash # => { a: true, b: false }
|
||||
def except!(*keys)
|
||||
keys.each { |key| delete(key) }
|
||||
self
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/hash_with_indifferent_access"
|
||||
|
||||
class Hash
|
||||
# Returns an <tt>ActiveSupport::HashWithIndifferentAccess</tt> out of its receiver:
|
||||
#
|
||||
# { a: 1 }.with_indifferent_access['a'] # => 1
|
||||
def with_indifferent_access
|
||||
ActiveSupport::HashWithIndifferentAccess.new(self)
|
||||
end
|
||||
|
||||
# Called when object is nested under an object that receives
|
||||
# #with_indifferent_access. This method will be called on the current object
|
||||
# by the enclosing object and is aliased to #with_indifferent_access by
|
||||
# default. Subclasses of Hash may overwrite this method to return +self+ if
|
||||
# converting to an <tt>ActiveSupport::HashWithIndifferentAccess</tt> would not be
|
||||
# desirable.
|
||||
#
|
||||
# b = { b: 1 }
|
||||
# { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access
|
||||
# # => {"b"=>1}
|
||||
alias nested_under_indifferent_access with_indifferent_access
|
||||
end
|
||||
@ -0,0 +1,172 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Hash
|
||||
# Returns a new hash with all keys converted using the +block+ operation.
|
||||
#
|
||||
# hash = { name: 'Rob', age: '28' }
|
||||
#
|
||||
# hash.transform_keys { |key| key.to_s.upcase } # => {"NAME"=>"Rob", "AGE"=>"28"}
|
||||
#
|
||||
# If you do not provide a +block+, it will return an Enumerator
|
||||
# for chaining with other methods:
|
||||
#
|
||||
# hash.transform_keys.with_index { |k, i| [k, i].join } # => {"name0"=>"Rob", "age1"=>"28"}
|
||||
def transform_keys
|
||||
return enum_for(:transform_keys) { size } unless block_given?
|
||||
result = {}
|
||||
each_key do |key|
|
||||
result[yield(key)] = self[key]
|
||||
end
|
||||
result
|
||||
end unless method_defined? :transform_keys
|
||||
|
||||
# Destructively converts all keys using the +block+ operations.
|
||||
# Same as +transform_keys+ but modifies +self+.
|
||||
def transform_keys!
|
||||
return enum_for(:transform_keys!) { size } unless block_given?
|
||||
keys.each do |key|
|
||||
self[yield(key)] = delete(key)
|
||||
end
|
||||
self
|
||||
end unless method_defined? :transform_keys!
|
||||
|
||||
# Returns a new hash with all keys converted to strings.
|
||||
#
|
||||
# hash = { name: 'Rob', age: '28' }
|
||||
#
|
||||
# hash.stringify_keys
|
||||
# # => {"name"=>"Rob", "age"=>"28"}
|
||||
def stringify_keys
|
||||
transform_keys(&:to_s)
|
||||
end
|
||||
|
||||
# Destructively converts all keys to strings. Same as
|
||||
# +stringify_keys+, but modifies +self+.
|
||||
def stringify_keys!
|
||||
transform_keys!(&:to_s)
|
||||
end
|
||||
|
||||
# Returns a new hash with all keys converted to symbols, as long as
|
||||
# they respond to +to_sym+.
|
||||
#
|
||||
# hash = { 'name' => 'Rob', 'age' => '28' }
|
||||
#
|
||||
# hash.symbolize_keys
|
||||
# # => {:name=>"Rob", :age=>"28"}
|
||||
def symbolize_keys
|
||||
transform_keys { |key| key.to_sym rescue key }
|
||||
end
|
||||
alias_method :to_options, :symbolize_keys
|
||||
|
||||
# Destructively converts all keys to symbols, as long as they respond
|
||||
# to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
|
||||
def symbolize_keys!
|
||||
transform_keys! { |key| key.to_sym rescue key }
|
||||
end
|
||||
alias_method :to_options!, :symbolize_keys!
|
||||
|
||||
# Validates all keys in a hash match <tt>*valid_keys</tt>, raising
|
||||
# +ArgumentError+ on a mismatch.
|
||||
#
|
||||
# Note that keys are treated differently than HashWithIndifferentAccess,
|
||||
# meaning that string and symbol keys will not match.
|
||||
#
|
||||
# { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
|
||||
# { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
|
||||
# { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
|
||||
def assert_valid_keys(*valid_keys)
|
||||
valid_keys.flatten!
|
||||
each_key do |k|
|
||||
unless valid_keys.include?(k)
|
||||
raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a new hash with all keys converted by the block operation.
|
||||
# This includes the keys from the root hash and from all
|
||||
# nested hashes and arrays.
|
||||
#
|
||||
# hash = { person: { name: 'Rob', age: '28' } }
|
||||
#
|
||||
# hash.deep_transform_keys{ |key| key.to_s.upcase }
|
||||
# # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
|
||||
def deep_transform_keys(&block)
|
||||
_deep_transform_keys_in_object(self, &block)
|
||||
end
|
||||
|
||||
# Destructively converts all keys by using the block operation.
|
||||
# This includes the keys from the root hash and from all
|
||||
# nested hashes and arrays.
|
||||
def deep_transform_keys!(&block)
|
||||
_deep_transform_keys_in_object!(self, &block)
|
||||
end
|
||||
|
||||
# Returns a new hash with all keys converted to strings.
|
||||
# This includes the keys from the root hash and from all
|
||||
# nested hashes and arrays.
|
||||
#
|
||||
# hash = { person: { name: 'Rob', age: '28' } }
|
||||
#
|
||||
# hash.deep_stringify_keys
|
||||
# # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
|
||||
def deep_stringify_keys
|
||||
deep_transform_keys(&:to_s)
|
||||
end
|
||||
|
||||
# Destructively converts all keys to strings.
|
||||
# This includes the keys from the root hash and from all
|
||||
# nested hashes and arrays.
|
||||
def deep_stringify_keys!
|
||||
deep_transform_keys!(&:to_s)
|
||||
end
|
||||
|
||||
# Returns a new hash with all keys converted to symbols, as long as
|
||||
# they respond to +to_sym+. This includes the keys from the root hash
|
||||
# and from all nested hashes and arrays.
|
||||
#
|
||||
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
|
||||
#
|
||||
# hash.deep_symbolize_keys
|
||||
# # => {:person=>{:name=>"Rob", :age=>"28"}}
|
||||
def deep_symbolize_keys
|
||||
deep_transform_keys { |key| key.to_sym rescue key }
|
||||
end
|
||||
|
||||
# Destructively converts all keys to symbols, as long as they respond
|
||||
# to +to_sym+. This includes the keys from the root hash and from all
|
||||
# nested hashes and arrays.
|
||||
def deep_symbolize_keys!
|
||||
deep_transform_keys! { |key| key.to_sym rescue key }
|
||||
end
|
||||
|
||||
private
|
||||
# support methods for deep transforming nested hashes and arrays
|
||||
def _deep_transform_keys_in_object(object, &block)
|
||||
case object
|
||||
when Hash
|
||||
object.each_with_object({}) do |(key, value), result|
|
||||
result[yield(key)] = _deep_transform_keys_in_object(value, &block)
|
||||
end
|
||||
when Array
|
||||
object.map { |e| _deep_transform_keys_in_object(e, &block) }
|
||||
else
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
def _deep_transform_keys_in_object!(object, &block)
|
||||
case object
|
||||
when Hash
|
||||
object.keys.each do |key|
|
||||
value = object.delete(key)
|
||||
object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
|
||||
end
|
||||
object
|
||||
when Array
|
||||
object.map! { |e| _deep_transform_keys_in_object!(e, &block) }
|
||||
else
|
||||
object
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Hash
|
||||
# Merges the caller into +other_hash+. For example,
|
||||
#
|
||||
# options = options.reverse_merge(size: 25, velocity: 10)
|
||||
#
|
||||
# is equivalent to
|
||||
#
|
||||
# options = { size: 25, velocity: 10 }.merge(options)
|
||||
#
|
||||
# This is particularly useful for initializing an options hash
|
||||
# with default values.
|
||||
def reverse_merge(other_hash)
|
||||
other_hash.merge(self)
|
||||
end
|
||||
alias_method :with_defaults, :reverse_merge
|
||||
|
||||
# Destructive +reverse_merge+.
|
||||
def reverse_merge!(other_hash)
|
||||
replace(reverse_merge(other_hash))
|
||||
end
|
||||
alias_method :reverse_update, :reverse_merge!
|
||||
alias_method :with_defaults!, :reverse_merge!
|
||||
end
|
||||
@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Hash
|
||||
# Slices a hash to include only the given keys. Returns a hash containing
|
||||
# the given keys.
|
||||
#
|
||||
# { a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b)
|
||||
# # => {:a=>1, :b=>2}
|
||||
#
|
||||
# This is useful for limiting an options hash to valid keys before
|
||||
# passing to a method:
|
||||
#
|
||||
# def search(criteria = {})
|
||||
# criteria.assert_valid_keys(:mass, :velocity, :time)
|
||||
# end
|
||||
#
|
||||
# search(options.slice(:mass, :velocity, :time))
|
||||
#
|
||||
# If you have an array of keys you want to limit to, you should splat them:
|
||||
#
|
||||
# valid_keys = [:mass, :velocity, :time]
|
||||
# search(options.slice(*valid_keys))
|
||||
def slice(*keys)
|
||||
keys.each_with_object(Hash.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
|
||||
end unless method_defined?(:slice)
|
||||
|
||||
# Replaces the hash with only the given keys.
|
||||
# Returns a hash containing the removed key/value pairs.
|
||||
#
|
||||
# { a: 1, b: 2, c: 3, d: 4 }.slice!(:a, :b)
|
||||
# # => {:c=>3, :d=>4}
|
||||
def slice!(*keys)
|
||||
omit = slice(*self.keys - keys)
|
||||
hash = slice(*keys)
|
||||
hash.default = default
|
||||
hash.default_proc = default_proc if default_proc
|
||||
replace(hash)
|
||||
omit
|
||||
end
|
||||
|
||||
# Removes and returns the key/value pairs matching the given keys.
|
||||
#
|
||||
# { a: 1, b: 2, c: 3, d: 4 }.extract!(:a, :b) # => {:a=>1, :b=>2}
|
||||
# { a: 1, b: 2 }.extract!(:a, :x) # => {:a=>1}
|
||||
def extract!(*keys)
|
||||
keys.each_with_object(self.class.new) { |key, result| result[key] = delete(key) if has_key?(key) }
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Hash
|
||||
# Returns a new hash with the results of running +block+ once for every value.
|
||||
# The keys are unchanged.
|
||||
#
|
||||
# { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } # => { a: 2, b: 4, c: 6 }
|
||||
#
|
||||
# If you do not provide a +block+, it will return an Enumerator
|
||||
# for chaining with other methods:
|
||||
#
|
||||
# { a: 1, b: 2 }.transform_values.with_index { |v, i| [v, i].join.to_i } # => { a: 10, b: 21 }
|
||||
def transform_values
|
||||
return enum_for(:transform_values) { size } unless block_given?
|
||||
return {} if empty?
|
||||
result = self.class.new
|
||||
each do |key, value|
|
||||
result[key] = yield(value)
|
||||
end
|
||||
result
|
||||
end unless method_defined? :transform_values
|
||||
|
||||
# Destructively converts all values using the +block+ operations.
|
||||
# Same as +transform_values+ but modifies +self+.
|
||||
def transform_values!
|
||||
return enum_for(:transform_values!) { size } unless block_given?
|
||||
each do |key, value|
|
||||
self[key] = yield(value)
|
||||
end
|
||||
end unless method_defined? :transform_values!
|
||||
# TODO: Remove this file when supporting only Ruby 2.4+.
|
||||
end
|
||||
@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/integer/multiple"
|
||||
require "active_support/core_ext/integer/inflections"
|
||||
require "active_support/core_ext/integer/time"
|
||||
@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/inflector"
|
||||
|
||||
class Integer
|
||||
# Ordinalize turns a number into an ordinal string used to denote the
|
||||
# position in an ordered sequence such as 1st, 2nd, 3rd, 4th.
|
||||
#
|
||||
# 1.ordinalize # => "1st"
|
||||
# 2.ordinalize # => "2nd"
|
||||
# 1002.ordinalize # => "1002nd"
|
||||
# 1003.ordinalize # => "1003rd"
|
||||
# -11.ordinalize # => "-11th"
|
||||
# -1001.ordinalize # => "-1001st"
|
||||
def ordinalize
|
||||
ActiveSupport::Inflector.ordinalize(self)
|
||||
end
|
||||
|
||||
# Ordinal returns the suffix used to denote the position
|
||||
# in an ordered sequence such as 1st, 2nd, 3rd, 4th.
|
||||
#
|
||||
# 1.ordinal # => "st"
|
||||
# 2.ordinal # => "nd"
|
||||
# 1002.ordinal # => "nd"
|
||||
# 1003.ordinal # => "rd"
|
||||
# -11.ordinal # => "th"
|
||||
# -1001.ordinal # => "st"
|
||||
def ordinal
|
||||
ActiveSupport::Inflector.ordinal(self)
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Integer
|
||||
# Check whether the integer is evenly divisible by the argument.
|
||||
#
|
||||
# 0.multiple_of?(0) # => true
|
||||
# 6.multiple_of?(5) # => false
|
||||
# 10.multiple_of?(2) # => true
|
||||
def multiple_of?(number)
|
||||
number != 0 ? self % number == 0 : zero?
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/duration"
|
||||
require "active_support/core_ext/numeric/time"
|
||||
|
||||
class Integer
|
||||
# Returns a Duration instance matching the number of months provided.
|
||||
#
|
||||
# 2.months # => 2 months
|
||||
def months
|
||||
ActiveSupport::Duration.months(self)
|
||||
end
|
||||
alias :month :months
|
||||
|
||||
# Returns a Duration instance matching the number of years provided.
|
||||
#
|
||||
# 2.years # => 2 years
|
||||
def years
|
||||
ActiveSupport::Duration.years(self)
|
||||
end
|
||||
alias :year :years
|
||||
end
|
||||
@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/kernel/agnostics"
|
||||
require "active_support/core_ext/kernel/concern"
|
||||
require "active_support/core_ext/kernel/reporting"
|
||||
require "active_support/core_ext/kernel/singleton_class"
|
||||
@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Object
|
||||
# Makes backticks behave (somewhat more) similarly on all platforms.
|
||||
# On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the
|
||||
# spawned shell prints a message to stderr and sets $?. We emulate
|
||||
# Unix on the former but not the latter.
|
||||
def `(command) #:nodoc:
|
||||
super
|
||||
rescue Errno::ENOENT => e
|
||||
STDERR.puts "#$0: #{e}"
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/module/concerning"
|
||||
|
||||
module Kernel
|
||||
module_function
|
||||
|
||||
# A shortcut to define a toplevel concern, not within a module.
|
||||
#
|
||||
# See Module::Concerning for more.
|
||||
def concern(topic, &module_definition)
|
||||
Object.concern topic, &module_definition
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Kernel
|
||||
module_function
|
||||
|
||||
# Sets $VERBOSE to +nil+ for the duration of the block and back to its original
|
||||
# value afterwards.
|
||||
#
|
||||
# silence_warnings do
|
||||
# value = noisy_call # no warning voiced
|
||||
# end
|
||||
#
|
||||
# noisy_call # warning voiced
|
||||
def silence_warnings
|
||||
with_warnings(nil) { yield }
|
||||
end
|
||||
|
||||
# Sets $VERBOSE to +true+ for the duration of the block and back to its
|
||||
# original value afterwards.
|
||||
def enable_warnings
|
||||
with_warnings(true) { yield }
|
||||
end
|
||||
|
||||
# Sets $VERBOSE for the duration of the block and back to its original
|
||||
# value afterwards.
|
||||
def with_warnings(flag)
|
||||
old_verbose, $VERBOSE = $VERBOSE, flag
|
||||
yield
|
||||
ensure
|
||||
$VERBOSE = old_verbose
|
||||
end
|
||||
|
||||
# Blocks and ignores any exception passed as argument if raised within the block.
|
||||
#
|
||||
# suppress(ZeroDivisionError) do
|
||||
# 1/0
|
||||
# puts 'This code is NOT reached'
|
||||
# end
|
||||
#
|
||||
# puts 'This code gets executed and nothing related to ZeroDivisionError was seen'
|
||||
def suppress(*exception_classes)
|
||||
yield
|
||||
rescue *exception_classes
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Kernel
|
||||
# class_eval on an object acts like singleton_class.class_eval.
|
||||
def class_eval(*args, &block)
|
||||
singleton_class.class_eval(*args, &block)
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LoadError
|
||||
# Returns true if the given path name (except perhaps for the ".rb"
|
||||
# extension) is the missing file which caused the exception to be raised.
|
||||
def is_missing?(location)
|
||||
location.sub(/\.rb$/, "".freeze) == path.sub(/\.rb$/, "".freeze)
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ActiveSupport
|
||||
module MarshalWithAutoloading # :nodoc:
|
||||
def load(source, proc = nil)
|
||||
super(source, proc)
|
||||
rescue ArgumentError, NameError => exc
|
||||
if exc.message.match(%r|undefined class/module (.+?)(?:::)?\z|)
|
||||
# try loading the class/module
|
||||
loaded = $1.constantize
|
||||
|
||||
raise unless $1 == loaded.name
|
||||
|
||||
# if it is an IO we need to go back to read the object
|
||||
source.rewind if source.respond_to?(:rewind)
|
||||
retry
|
||||
else
|
||||
raise exc
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Marshal.singleton_class.prepend(ActiveSupport::MarshalWithAutoloading)
|
||||
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/module/aliasing"
|
||||
require "active_support/core_ext/module/introspection"
|
||||
require "active_support/core_ext/module/anonymous"
|
||||
require "active_support/core_ext/module/reachable"
|
||||
require "active_support/core_ext/module/attribute_accessors"
|
||||
require "active_support/core_ext/module/attribute_accessors_per_thread"
|
||||
require "active_support/core_ext/module/attr_internal"
|
||||
require "active_support/core_ext/module/concerning"
|
||||
require "active_support/core_ext/module/delegation"
|
||||
require "active_support/core_ext/module/deprecation"
|
||||
require "active_support/core_ext/module/redefine_method"
|
||||
require "active_support/core_ext/module/remove_method"
|
||||
@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Module
|
||||
# Allows you to make aliases for attributes, which includes
|
||||
# getter, setter, and a predicate.
|
||||
#
|
||||
# class Content < ActiveRecord::Base
|
||||
# # has a title attribute
|
||||
# end
|
||||
#
|
||||
# class Email < Content
|
||||
# alias_attribute :subject, :title
|
||||
# end
|
||||
#
|
||||
# e = Email.find(1)
|
||||
# e.title # => "Superstars"
|
||||
# e.subject # => "Superstars"
|
||||
# e.subject? # => true
|
||||
# e.subject = "Megastars"
|
||||
# e.title # => "Megastars"
|
||||
def alias_attribute(new_name, old_name)
|
||||
# The following reader methods use an explicit `self` receiver in order to
|
||||
# support aliases that start with an uppercase letter. Otherwise, they would
|
||||
# be resolved as constants instead.
|
||||
module_eval <<-STR, __FILE__, __LINE__ + 1
|
||||
def #{new_name}; self.#{old_name}; end # def subject; self.title; end
|
||||
def #{new_name}?; self.#{old_name}?; end # def subject?; self.title?; end
|
||||
def #{new_name}=(v); self.#{old_name} = v; end # def subject=(v); self.title = v; end
|
||||
STR
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Module
|
||||
# A module may or may not have a name.
|
||||
#
|
||||
# module M; end
|
||||
# M.name # => "M"
|
||||
#
|
||||
# m = Module.new
|
||||
# m.name # => nil
|
||||
#
|
||||
# +anonymous?+ method returns true if module does not have a name, false otherwise:
|
||||
#
|
||||
# Module.new.anonymous? # => true
|
||||
#
|
||||
# module M; end
|
||||
# M.anonymous? # => false
|
||||
#
|
||||
# A module gets a name when it is first assigned to a constant. Either
|
||||
# via the +module+ or +class+ keyword or by an explicit assignment:
|
||||
#
|
||||
# m = Module.new # creates an anonymous module
|
||||
# m.anonymous? # => true
|
||||
# M = m # m gets a name here as a side-effect
|
||||
# m.name # => "M"
|
||||
# m.anonymous? # => false
|
||||
def anonymous?
|
||||
name.nil?
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Module
|
||||
# Declares an attribute reader backed by an internally-named instance variable.
|
||||
def attr_internal_reader(*attrs)
|
||||
attrs.each { |attr_name| attr_internal_define(attr_name, :reader) }
|
||||
end
|
||||
|
||||
# Declares an attribute writer backed by an internally-named instance variable.
|
||||
def attr_internal_writer(*attrs)
|
||||
attrs.each { |attr_name| attr_internal_define(attr_name, :writer) }
|
||||
end
|
||||
|
||||
# Declares an attribute reader and writer backed by an internally-named instance
|
||||
# variable.
|
||||
def attr_internal_accessor(*attrs)
|
||||
attr_internal_reader(*attrs)
|
||||
attr_internal_writer(*attrs)
|
||||
end
|
||||
alias_method :attr_internal, :attr_internal_accessor
|
||||
|
||||
class << self; attr_accessor :attr_internal_naming_format end
|
||||
self.attr_internal_naming_format = "@_%s"
|
||||
|
||||
private
|
||||
def attr_internal_ivar_name(attr)
|
||||
Module.attr_internal_naming_format % attr
|
||||
end
|
||||
|
||||
def attr_internal_define(attr_name, type)
|
||||
internal_name = attr_internal_ivar_name(attr_name).sub(/\A@/, "")
|
||||
# use native attr_* methods as they are faster on some Ruby implementations
|
||||
send("attr_#{type}", internal_name)
|
||||
attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer
|
||||
alias_method attr_name, internal_name
|
||||
remove_method internal_name
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,215 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/array/extract_options"
|
||||
require "active_support/core_ext/regexp"
|
||||
|
||||
# Extends the module object with class/module and instance accessors for
|
||||
# class/module attributes, just like the native attr* accessors for instance
|
||||
# attributes.
|
||||
class Module
|
||||
# Defines a class attribute and creates a class and instance reader methods.
|
||||
# The underlying class variable is set to +nil+, if it is not previously
|
||||
# defined. All class and instance methods created will be public, even if
|
||||
# this method is called with a private or protected access modifier.
|
||||
#
|
||||
# module HairColors
|
||||
# mattr_reader :hair_colors
|
||||
# end
|
||||
#
|
||||
# HairColors.hair_colors # => nil
|
||||
# HairColors.class_variable_set("@@hair_colors", [:brown, :black])
|
||||
# HairColors.hair_colors # => [:brown, :black]
|
||||
#
|
||||
# The attribute name must be a valid method name in Ruby.
|
||||
#
|
||||
# module Foo
|
||||
# mattr_reader :"1_Badname"
|
||||
# end
|
||||
# # => NameError: invalid attribute name: 1_Badname
|
||||
#
|
||||
# If you want to opt out the creation on the instance reader method, pass
|
||||
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
|
||||
#
|
||||
# module HairColors
|
||||
# mattr_reader :hair_colors, instance_reader: false
|
||||
# end
|
||||
#
|
||||
# class Person
|
||||
# include HairColors
|
||||
# end
|
||||
#
|
||||
# Person.new.hair_colors # => NoMethodError
|
||||
#
|
||||
# You can set a default value for the attribute.
|
||||
#
|
||||
# module HairColors
|
||||
# mattr_reader :hair_colors, default: [:brown, :black, :blonde, :red]
|
||||
# end
|
||||
#
|
||||
# class Person
|
||||
# include HairColors
|
||||
# end
|
||||
#
|
||||
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
|
||||
def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil)
|
||||
syms.each do |sym|
|
||||
raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
@@#{sym} = nil unless defined? @@#{sym}
|
||||
|
||||
def self.#{sym}
|
||||
@@#{sym}
|
||||
end
|
||||
EOS
|
||||
|
||||
if instance_reader && instance_accessor
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def #{sym}
|
||||
@@#{sym}
|
||||
end
|
||||
EOS
|
||||
end
|
||||
|
||||
sym_default_value = (block_given? && default.nil?) ? yield : default
|
||||
class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil?
|
||||
end
|
||||
end
|
||||
alias :cattr_reader :mattr_reader
|
||||
|
||||
# Defines a class attribute and creates a class and instance writer methods to
|
||||
# allow assignment to the attribute. All class and instance methods created
|
||||
# will be public, even if this method is called with a private or protected
|
||||
# access modifier.
|
||||
#
|
||||
# module HairColors
|
||||
# mattr_writer :hair_colors
|
||||
# end
|
||||
#
|
||||
# class Person
|
||||
# include HairColors
|
||||
# end
|
||||
#
|
||||
# HairColors.hair_colors = [:brown, :black]
|
||||
# Person.class_variable_get("@@hair_colors") # => [:brown, :black]
|
||||
# Person.new.hair_colors = [:blonde, :red]
|
||||
# HairColors.class_variable_get("@@hair_colors") # => [:blonde, :red]
|
||||
#
|
||||
# If you want to opt out the instance writer method, pass
|
||||
# <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
|
||||
#
|
||||
# module HairColors
|
||||
# mattr_writer :hair_colors, instance_writer: false
|
||||
# end
|
||||
#
|
||||
# class Person
|
||||
# include HairColors
|
||||
# end
|
||||
#
|
||||
# Person.new.hair_colors = [:blonde, :red] # => NoMethodError
|
||||
#
|
||||
# You can set a default value for the attribute.
|
||||
#
|
||||
# module HairColors
|
||||
# mattr_writer :hair_colors, default: [:brown, :black, :blonde, :red]
|
||||
# end
|
||||
#
|
||||
# class Person
|
||||
# include HairColors
|
||||
# end
|
||||
#
|
||||
# Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
|
||||
def mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil)
|
||||
syms.each do |sym|
|
||||
raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
@@#{sym} = nil unless defined? @@#{sym}
|
||||
|
||||
def self.#{sym}=(obj)
|
||||
@@#{sym} = obj
|
||||
end
|
||||
EOS
|
||||
|
||||
if instance_writer && instance_accessor
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def #{sym}=(obj)
|
||||
@@#{sym} = obj
|
||||
end
|
||||
EOS
|
||||
end
|
||||
|
||||
sym_default_value = (block_given? && default.nil?) ? yield : default
|
||||
send("#{sym}=", sym_default_value) unless sym_default_value.nil?
|
||||
end
|
||||
end
|
||||
alias :cattr_writer :mattr_writer
|
||||
|
||||
# Defines both class and instance accessors for class attributes.
|
||||
# All class and instance methods created will be public, even if
|
||||
# this method is called with a private or protected access modifier.
|
||||
#
|
||||
# module HairColors
|
||||
# mattr_accessor :hair_colors
|
||||
# end
|
||||
#
|
||||
# class Person
|
||||
# include HairColors
|
||||
# end
|
||||
#
|
||||
# HairColors.hair_colors = [:brown, :black, :blonde, :red]
|
||||
# HairColors.hair_colors # => [:brown, :black, :blonde, :red]
|
||||
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
|
||||
#
|
||||
# If a subclass changes the value then that would also change the value for
|
||||
# parent class. Similarly if parent class changes the value then that would
|
||||
# change the value of subclasses too.
|
||||
#
|
||||
# class Male < Person
|
||||
# end
|
||||
#
|
||||
# Male.new.hair_colors << :blue
|
||||
# Person.new.hair_colors # => [:brown, :black, :blonde, :red, :blue]
|
||||
#
|
||||
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
|
||||
# To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
|
||||
#
|
||||
# module HairColors
|
||||
# mattr_accessor :hair_colors, instance_writer: false, instance_reader: false
|
||||
# end
|
||||
#
|
||||
# class Person
|
||||
# include HairColors
|
||||
# end
|
||||
#
|
||||
# Person.new.hair_colors = [:brown] # => NoMethodError
|
||||
# Person.new.hair_colors # => NoMethodError
|
||||
#
|
||||
# Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods.
|
||||
#
|
||||
# module HairColors
|
||||
# mattr_accessor :hair_colors, instance_accessor: false
|
||||
# end
|
||||
#
|
||||
# class Person
|
||||
# include HairColors
|
||||
# end
|
||||
#
|
||||
# Person.new.hair_colors = [:brown] # => NoMethodError
|
||||
# Person.new.hair_colors # => NoMethodError
|
||||
#
|
||||
# You can set a default value for the attribute.
|
||||
#
|
||||
# module HairColors
|
||||
# mattr_accessor :hair_colors, default: [:brown, :black, :blonde, :red]
|
||||
# end
|
||||
#
|
||||
# class Person
|
||||
# include HairColors
|
||||
# end
|
||||
#
|
||||
# Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
|
||||
def mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil, &blk)
|
||||
mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default, &blk)
|
||||
mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor, default: default)
|
||||
end
|
||||
alias :cattr_accessor :mattr_accessor
|
||||
end
|
||||
@ -0,0 +1,150 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/array/extract_options"
|
||||
require "active_support/core_ext/regexp"
|
||||
|
||||
# Extends the module object with class/module and instance accessors for
|
||||
# class/module attributes, just like the native attr* accessors for instance
|
||||
# attributes, but does so on a per-thread basis.
|
||||
#
|
||||
# So the values are scoped within the Thread.current space under the class name
|
||||
# of the module.
|
||||
class Module
|
||||
# Defines a per-thread class attribute and creates class and instance reader methods.
|
||||
# The underlying per-thread class variable is set to +nil+, if it is not previously defined.
|
||||
#
|
||||
# module Current
|
||||
# thread_mattr_reader :user
|
||||
# end
|
||||
#
|
||||
# Current.user # => nil
|
||||
# Thread.current[:attr_Current_user] = "DHH"
|
||||
# Current.user # => "DHH"
|
||||
#
|
||||
# The attribute name must be a valid method name in Ruby.
|
||||
#
|
||||
# module Foo
|
||||
# thread_mattr_reader :"1_Badname"
|
||||
# end
|
||||
# # => NameError: invalid attribute name: 1_Badname
|
||||
#
|
||||
# If you want to opt out of the creation of the instance reader method, pass
|
||||
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
|
||||
#
|
||||
# class Current
|
||||
# thread_mattr_reader :user, instance_reader: false
|
||||
# end
|
||||
#
|
||||
# Current.new.user # => NoMethodError
|
||||
def thread_mattr_reader(*syms) # :nodoc:
|
||||
options = syms.extract_options!
|
||||
|
||||
syms.each do |sym|
|
||||
raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
|
||||
|
||||
# The following generated method concatenates `name` because we want it
|
||||
# to work with inheritance via polymorphism.
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def self.#{sym}
|
||||
Thread.current["attr_" + name + "_#{sym}"]
|
||||
end
|
||||
EOS
|
||||
|
||||
unless options[:instance_reader] == false || options[:instance_accessor] == false
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def #{sym}
|
||||
self.class.#{sym}
|
||||
end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
end
|
||||
alias :thread_cattr_reader :thread_mattr_reader
|
||||
|
||||
# Defines a per-thread class attribute and creates a class and instance writer methods to
|
||||
# allow assignment to the attribute.
|
||||
#
|
||||
# module Current
|
||||
# thread_mattr_writer :user
|
||||
# end
|
||||
#
|
||||
# Current.user = "DHH"
|
||||
# Thread.current[:attr_Current_user] # => "DHH"
|
||||
#
|
||||
# If you want to opt out of the creation of the instance writer method, pass
|
||||
# <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
|
||||
#
|
||||
# class Current
|
||||
# thread_mattr_writer :user, instance_writer: false
|
||||
# end
|
||||
#
|
||||
# Current.new.user = "DHH" # => NoMethodError
|
||||
def thread_mattr_writer(*syms) # :nodoc:
|
||||
options = syms.extract_options!
|
||||
syms.each do |sym|
|
||||
raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
|
||||
|
||||
# The following generated method concatenates `name` because we want it
|
||||
# to work with inheritance via polymorphism.
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def self.#{sym}=(obj)
|
||||
Thread.current["attr_" + name + "_#{sym}"] = obj
|
||||
end
|
||||
EOS
|
||||
|
||||
unless options[:instance_writer] == false || options[:instance_accessor] == false
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def #{sym}=(obj)
|
||||
self.class.#{sym} = obj
|
||||
end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
end
|
||||
alias :thread_cattr_writer :thread_mattr_writer
|
||||
|
||||
# Defines both class and instance accessors for class attributes.
|
||||
#
|
||||
# class Account
|
||||
# thread_mattr_accessor :user
|
||||
# end
|
||||
#
|
||||
# Account.user = "DHH"
|
||||
# Account.user # => "DHH"
|
||||
# Account.new.user # => "DHH"
|
||||
#
|
||||
# If a subclass changes the value, the parent class' value is not changed.
|
||||
# Similarly, if the parent class changes the value, the value of subclasses
|
||||
# is not changed.
|
||||
#
|
||||
# class Customer < Account
|
||||
# end
|
||||
#
|
||||
# Customer.user = "Rafael"
|
||||
# Customer.user # => "Rafael"
|
||||
# Account.user # => "DHH"
|
||||
#
|
||||
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
|
||||
# To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
|
||||
#
|
||||
# class Current
|
||||
# thread_mattr_accessor :user, instance_writer: false, instance_reader: false
|
||||
# end
|
||||
#
|
||||
# Current.new.user = "DHH" # => NoMethodError
|
||||
# Current.new.user # => NoMethodError
|
||||
#
|
||||
# Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods.
|
||||
#
|
||||
# class Current
|
||||
# mattr_accessor :user, instance_accessor: false
|
||||
# end
|
||||
#
|
||||
# Current.new.user = "DHH" # => NoMethodError
|
||||
# Current.new.user # => NoMethodError
|
||||
def thread_mattr_accessor(*syms)
|
||||
thread_mattr_reader(*syms)
|
||||
thread_mattr_writer(*syms)
|
||||
end
|
||||
alias :thread_cattr_accessor :thread_mattr_accessor
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user