Merge pull request #4920 from reitermarkus/days

Use ActiveSupport’s `#days`.
This commit is contained in:
Markus Reiter 2018-10-08 15:14:36 +02:00 committed by GitHub
commit 7cb1ceb756
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
236 changed files with 18018 additions and 205 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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}"

View File

@ -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

View File

@ -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

View File

@ -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}",

View 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|

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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,

View 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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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, '&nbsp;') {|group| p group}
# ["1", "2"]
# ["3", "4"]
# ["5", "&nbsp;"]
#
# %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, '&nbsp;') {|group| p group}
# ["1", "2", "3", "4"]
# ["5", "6", "7", "&nbsp;"]
# ["8", "9", "10", "&nbsp;"]
#
# %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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
require "active_support/core_ext/big_decimal/conversions"

View File

@ -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)

View File

@ -0,0 +1,4 @@
# frozen_string_literal: true
require "active_support/core_ext/class/attribute"
require "active_support/core_ext/class/subclasses"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
require "active_support/core_ext/file/atomic"

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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