rails_extend 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +20 -0
- data/README.md +46 -0
- data/config/locales/en.datetime.yml +17 -0
- data/config/locales/zh.datetime.yml +145 -0
- data/config/locales/zh.support.yml +52 -0
- data/lib/generators/rails_extend/ignore_generator.rb +27 -0
- data/lib/generators/rails_extend/migrations_generator.rb +33 -0
- data/lib/generators/rails_extend/remove_table_generator.rb +29 -0
- data/lib/generators/rails_extend/rename_module_generator.rb +29 -0
- data/lib/generators/rails_extend/templates/README.md +2 -0
- data/lib/generators/rails_extend/templates/initializer.rb.tt +10 -0
- data/lib/generators/rails_extend/templates/migration.rb.tt +35 -0
- data/lib/generators/rails_extend/templates/remove_table.rb.tt +9 -0
- data/lib/generators/rails_extend/templates/rename_module.rb.tt +9 -0
- data/lib/rails_extend/active_model/type_value.rb +26 -0
- data/lib/rails_extend/active_model.rb +3 -0
- data/lib/rails_extend/active_record/enum.rb +74 -0
- data/lib/rails_extend/active_record/extend.rb +200 -0
- data/lib/rails_extend/active_record/i18n.rb +31 -0
- data/lib/rails_extend/active_record/include.rb +29 -0
- data/lib/rails_extend/active_record/taxon.rb +29 -0
- data/lib/rails_extend/active_record/translation.rb +41 -0
- data/lib/rails_extend/active_record.rb +8 -0
- data/lib/rails_extend/active_storage/attachment.rb +16 -0
- data/lib/rails_extend/active_storage/blob_prepend.rb +40 -0
- data/lib/rails_extend/active_storage/variant.rb +16 -0
- data/lib/rails_extend/active_storage.rb +5 -0
- data/lib/rails_extend/config.rb +19 -0
- data/lib/rails_extend/core/array.rb +79 -0
- data/lib/rails_extend/core/date.rb +32 -0
- data/lib/rails_extend/core/duration.rb +12 -0
- data/lib/rails_extend/core/hash.rb +106 -0
- data/lib/rails_extend/core/module.rb +7 -0
- data/lib/rails_extend/core/nil.rb +11 -0
- data/lib/rails_extend/core/numeric.rb +11 -0
- data/lib/rails_extend/core/pathname.rb +15 -0
- data/lib/rails_extend/core/string.rb +15 -0
- data/lib/rails_extend/core/time_format.rb +20 -0
- data/lib/rails_extend/core.rb +17 -0
- data/lib/rails_extend/engine.rb +13 -0
- data/lib/rails_extend/env.rb +10 -0
- data/lib/rails_extend/generators/named_base.rb +22 -0
- data/lib/rails_extend/generators.rb +3 -0
- data/lib/rails_extend/models.rb +120 -0
- data/lib/rails_extend/quiet_logs.rb +22 -0
- data/lib/rails_extend/routes.rb +81 -0
- data/lib/rails_extend/type/i18n.rb +23 -0
- data/lib/rails_extend/type/taxon.rb +15 -0
- data/lib/rails_extend/type.rb +4 -0
- data/lib/rails_extend.rb +19 -0
- data/lib/templates/test_unit/model/fixtures.yml.tt +15 -0
- data/lib/templates/test_unit/model/unit_test.rb.tt +7 -0
- metadata +56 -5
- data/lib/active_model/type_value.rb +0 -20
@@ -0,0 +1,200 @@
|
|
1
|
+
module RailsExtend::ActiveRecord
|
2
|
+
module Extend
|
3
|
+
|
4
|
+
def human_name
|
5
|
+
model_name.human
|
6
|
+
end
|
7
|
+
|
8
|
+
def subclasses_tree(tree = {}, node = self)
|
9
|
+
tree[node] ||= {}
|
10
|
+
|
11
|
+
node.subclasses.each do |subclass|
|
12
|
+
tree[node].merge! subclasses_tree(tree[node], subclass)
|
13
|
+
end
|
14
|
+
|
15
|
+
tree
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_fixture
|
19
|
+
require 'rails/generators/test_unit/model/model_generator'
|
20
|
+
|
21
|
+
args = [
|
22
|
+
self.name.underscore
|
23
|
+
]
|
24
|
+
cols = columns.reject(&->(i){ attributes_by_default.include?(i.name) }).map { |col| "#{col.name}:#{col.type}" }
|
25
|
+
|
26
|
+
generator = TestUnit::Generators::ModelGenerator.new(args + cols, destination_root: Rails.root, fixture: true)
|
27
|
+
generator.instance_variable_set :@source_paths, Array(RailsExtend::Engine.root.join('lib/templates', 'test_unit/model'))
|
28
|
+
generator.invoke_all
|
29
|
+
end
|
30
|
+
|
31
|
+
def com_column_names
|
32
|
+
column_names - attributes_by_default + attachment_reflections.keys
|
33
|
+
end
|
34
|
+
|
35
|
+
def column_attributes
|
36
|
+
columns.map do |column|
|
37
|
+
r = {
|
38
|
+
name: column.name.to_sym,
|
39
|
+
type: column.type
|
40
|
+
}
|
41
|
+
r.merge! null: column.null unless column.null
|
42
|
+
r.merge! default: column.default unless column.default.nil?
|
43
|
+
r.merge! comment: column.comment if column.comment.present?
|
44
|
+
r.merge! column.sql_type_metadata.instance_values.slice('limit', 'precision', 'scale').compact
|
45
|
+
r.symbolize_keys!
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def attributes_by_model
|
50
|
+
cols = {}
|
51
|
+
|
52
|
+
attributes_to_define_after_schema_loads.each do |name, column|
|
53
|
+
r = {}
|
54
|
+
r.merge! type: column[0]
|
55
|
+
|
56
|
+
if r[:type].respond_to? :call
|
57
|
+
r.merge! type: r[:type].call(ActiveModel::Type::String.new)
|
58
|
+
end
|
59
|
+
|
60
|
+
if r[:type].respond_to?(:type)
|
61
|
+
r.merge! raw_type: r[:type].type
|
62
|
+
else
|
63
|
+
r.merge! raw_type: r[:type] # 兼容 rails 7 以下
|
64
|
+
end
|
65
|
+
|
66
|
+
case r[:type].class.name
|
67
|
+
when 'ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array'
|
68
|
+
r.merge! array: true
|
69
|
+
when 'ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Range'
|
70
|
+
r.merge! range: true
|
71
|
+
end
|
72
|
+
|
73
|
+
if r[:type].respond_to?(:options) && r[:type].options.present?
|
74
|
+
r.merge! r[:type].options
|
75
|
+
end
|
76
|
+
|
77
|
+
if Rails::VERSION::MAJOR >= 7 && !column[1].instance_of?(Object) # Rails 7, column[1] 为默认值
|
78
|
+
r.merge! default: column[1]
|
79
|
+
elsif Rails::VERSION::MAJOR < 7 # rails 7 以下, column[1] 为 options
|
80
|
+
r.merge! column[1]
|
81
|
+
end
|
82
|
+
|
83
|
+
cols.merge! name => r
|
84
|
+
end
|
85
|
+
|
86
|
+
cols
|
87
|
+
end
|
88
|
+
|
89
|
+
def migrate_attributes_by_model
|
90
|
+
cols = {}
|
91
|
+
attributes_by_model.each do |name, column|
|
92
|
+
r = {}
|
93
|
+
r.merge! column
|
94
|
+
r.merge! migrate_type: column[:raw_type]
|
95
|
+
r.symbolize_keys!
|
96
|
+
|
97
|
+
if r[:type].respond_to? :migrate_type
|
98
|
+
r.merge! migrate_type: r[:type].migrate_type
|
99
|
+
end
|
100
|
+
|
101
|
+
if r[:default].respond_to?(:call)
|
102
|
+
r.delete(:default)
|
103
|
+
end
|
104
|
+
|
105
|
+
if r[:array] && connection.adapter_name != 'PostgreSQL'
|
106
|
+
r.delete(:array)
|
107
|
+
r[:migrate_type] = :json
|
108
|
+
r.delete(:default) if r[:default].is_a? Array
|
109
|
+
end
|
110
|
+
|
111
|
+
if r[:migrate_type] == :json
|
112
|
+
if connection.adapter_name == 'PostgreSQL' # Postgres 替换 json 为 jsonb
|
113
|
+
r[:migrate_type] = :jsonb
|
114
|
+
else
|
115
|
+
r.delete(:default) # mysql 数据库不能接受 json 的默认值
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
if r[:migrate_type] == :jsonb && connection.adapter_name != 'PostgreSQL'
|
120
|
+
r[:migrate_type] == :json
|
121
|
+
r.delete(:default)
|
122
|
+
end
|
123
|
+
|
124
|
+
r.merge! attribute_options: r.slice(:limit, :precision, :scale, :null, :index, :array, :range, :size, :default, :comment).inject('') { |s, h| s << ", #{h[0]}: #{h[1].inspect}" }
|
125
|
+
|
126
|
+
cols.merge! name => r
|
127
|
+
end
|
128
|
+
|
129
|
+
cols
|
130
|
+
end
|
131
|
+
|
132
|
+
def migrate_attributes_by_db
|
133
|
+
return {} unless table_exists?
|
134
|
+
cols = {}
|
135
|
+
|
136
|
+
columns_hash.each do |name, column|
|
137
|
+
r = { type: column.type }
|
138
|
+
r.merge! migrate_type: r[:type]
|
139
|
+
r.merge! null: column.null unless column.null
|
140
|
+
r.merge! default: column.default unless column.default.nil?
|
141
|
+
r.merge! comment: column.comment if column.comment.present?
|
142
|
+
r.merge! column.sql_type_metadata.instance_values.slice('limit', 'precision', 'scale').compact
|
143
|
+
r.symbolize_keys!
|
144
|
+
r.merge! attribute_options: r.slice(:limit, :precision, :scale, :null, :index, :array, :size, :default, :comment).inject('') { |s, h| s << ", #{h[0]}: #{h[1].inspect}" }
|
145
|
+
|
146
|
+
cols.merge! name => r
|
147
|
+
end
|
148
|
+
|
149
|
+
cols
|
150
|
+
end
|
151
|
+
|
152
|
+
def attributes_by_default
|
153
|
+
if table_exists?
|
154
|
+
[primary_key] + all_timestamp_attributes_in_model
|
155
|
+
else
|
156
|
+
[]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def attributes_by_belongs
|
161
|
+
results = {}
|
162
|
+
|
163
|
+
reflections.values.select(&->(reflection){ reflection.belongs_to? }).each do |reflection|
|
164
|
+
results.merge! reflection.foreign_key => {
|
165
|
+
input_type: :integer # todo 考虑 foreign_key 不是自增 ID 的场景
|
166
|
+
}
|
167
|
+
results.merge! reflection.foreign_type => { input_type: :string } if reflection.foreign_type
|
168
|
+
end
|
169
|
+
results.except! *attributes_by_model.keys.map(&:to_s)
|
170
|
+
|
171
|
+
results
|
172
|
+
end
|
173
|
+
|
174
|
+
def references_by_model
|
175
|
+
results = {}
|
176
|
+
refs = reflections.values.select(&->(reflection){ reflection.belongs_to? })
|
177
|
+
refs.reject! { |reflection| reflection.foreign_key.to_s != "#{reflection.name}_id" }
|
178
|
+
refs.each do |ref|
|
179
|
+
r = { name: ref.name }
|
180
|
+
r.merge! polymorphic: true if ref.polymorphic?
|
181
|
+
r.merge! reference_options: r.slice(:polymorphic).inject('') { |s, h| s << ", #{h[0]}: #{h[1].inspect}" }
|
182
|
+
results[ref.foreign_key] = r unless results.key?(ref.foreign_key.to_sym)
|
183
|
+
end
|
184
|
+
|
185
|
+
results
|
186
|
+
end
|
187
|
+
|
188
|
+
def indexes_by_model
|
189
|
+
indexes = indexes_to_define_after_schema_loads
|
190
|
+
indexes.map! do |index|
|
191
|
+
index.merge! index_options: index.slice(:unique, :name).inject('') { |s, h| s << ", #{h[0]}: #{h[1].inspect}" }
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
ActiveSupport.on_load :active_record do
|
199
|
+
extend RailsExtend::ActiveRecord::Extend
|
200
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsExtend::ActiveRecord
|
4
|
+
module I18n
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_update :update_i18n_column
|
9
|
+
end
|
10
|
+
|
11
|
+
def update_i18n_column
|
12
|
+
str = []
|
13
|
+
self.changes.slice(*i18n_attributes).each do |key, _|
|
14
|
+
value = self.public_send("#{key}_before_type_cast")
|
15
|
+
str << "#{key} = #{key}::jsonb || '#{value.to_json}'::jsonb"
|
16
|
+
end
|
17
|
+
return if str.blank?
|
18
|
+
s = str.join(', ')
|
19
|
+
self.class.connection.execute "UPDATE #{self.class.table_name} SET #{s} WHERE id = #{self.id}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def attributes_with_values_for_create(attribute_names)
|
23
|
+
r = super
|
24
|
+
r.slice(*i18n_attributes).each do |key, v|
|
25
|
+
r[key] = public_send "#{key}_before_type_cast"
|
26
|
+
end
|
27
|
+
r
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module RailsExtend::ActiveRecord
|
2
|
+
module Include
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :indexes_to_define_after_schema_loads, instance_accessor: false, default: []
|
7
|
+
end
|
8
|
+
|
9
|
+
def error_text
|
10
|
+
errors.full_messages.join("\n")
|
11
|
+
end
|
12
|
+
|
13
|
+
def class_name
|
14
|
+
self.class.base_class.name
|
15
|
+
end
|
16
|
+
|
17
|
+
class_methods do
|
18
|
+
def index(name, **options)
|
19
|
+
h = { index: name, **options }
|
20
|
+
self.indexes_to_define_after_schema_loads = self.indexes_to_define_after_schema_loads + [h]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
ActiveSupport.on_load :active_record do
|
28
|
+
include RailsExtend::ActiveRecord::Include
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsExtend::ActiveRecord
|
4
|
+
module Taxon
|
5
|
+
|
6
|
+
def has_taxons(*columns)
|
7
|
+
columns.each do |column|
|
8
|
+
attribute "#{column}_ancestors", :taxon, outer: column
|
9
|
+
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
10
|
+
before_validation :sync_#{column}_id, if: -> { #{column}_ancestors_changed? }
|
11
|
+
|
12
|
+
def sync_#{column}_id
|
13
|
+
_outer_id = Hash(#{column}_ancestors).values.compact.last
|
14
|
+
if _outer_id
|
15
|
+
self.#{column}_id = _outer_id
|
16
|
+
else
|
17
|
+
self.#{column}_id = nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
RUBY_EVAL
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
ActiveSupport.on_load :active_record do
|
28
|
+
extend RailsExtend::ActiveRecord::Taxon
|
29
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsExtend::ActiveRecord
|
4
|
+
module Translation
|
5
|
+
|
6
|
+
# name
|
7
|
+
# * store as jsonb in database;
|
8
|
+
# * read with i18n scope
|
9
|
+
def has_translations(*columns)
|
10
|
+
mattr_accessor :i18n_attributes
|
11
|
+
self.i18n_attributes = columns.map(&:to_s)
|
12
|
+
include RailsCom::I18n
|
13
|
+
columns.each do |column|
|
14
|
+
attribute column, :i18n
|
15
|
+
|
16
|
+
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
17
|
+
|
18
|
+
def #{column}=(value)
|
19
|
+
if value.is_a?(String)
|
20
|
+
super(::I18n.locale.to_s => value)
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
RUBY_EVAL
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def _update_record(values, constraints)
|
32
|
+
values.except!(*i18n_attributes) if self.respond_to?(:i18n_attributes)
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
ActiveSupport.on_load :active_record do
|
40
|
+
extend RailsExtend::ActiveRecord::Translation
|
41
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_extend/active_record/enum'
|
4
|
+
require 'rails_extend/active_record/extend'
|
5
|
+
require 'rails_extend/active_record/i18n'
|
6
|
+
require 'rails_extend/active_record/include'
|
7
|
+
require 'rails_extend/active_record/taxon'
|
8
|
+
require 'rails_extend/active_record/translation'
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsExtend::ActiveStorage
|
4
|
+
module Attachment
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
attribute :name, :string, null: false
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
ActiveSupport.on_load(:active_storage_blob) do
|
15
|
+
ActiveStorage::Attachment.include RailsExtend::ActiveStorage::Attachment
|
16
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsExtend::ActiveStorage
|
4
|
+
module BlobPrepend
|
5
|
+
|
6
|
+
def self.prepended(klass)
|
7
|
+
klass.attribute :key, :string, null: false, index: { unique: true }
|
8
|
+
klass.attribute :filename, :string, null: false
|
9
|
+
klass.attribute :content_type, :string
|
10
|
+
klass.attribute :metadata, :json
|
11
|
+
klass.attribute :byte_size, :integer, null: false
|
12
|
+
klass.attribute :checksum, :string
|
13
|
+
klass.attribute :service_name, :string, null: false
|
14
|
+
klass.attribute :created_at, :datetime, null: false
|
15
|
+
end
|
16
|
+
|
17
|
+
def data_url
|
18
|
+
return @data_url if defined? @data_url
|
19
|
+
@data_url = "data:#{content_type};base64,#{Base64.encode64(download)}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def duration
|
23
|
+
(metadata[:duration] || 0).to_f
|
24
|
+
end
|
25
|
+
|
26
|
+
def duration_str
|
27
|
+
rh = TimeHelper.exact_distance_time(duration)
|
28
|
+
"#{rh[:minute]}:#{rh[:second].to_s.rjust(2, '0')}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def identify_later
|
32
|
+
ActiveStorage::IdentifyJob.perform_later(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
ActiveSupport.on_load(:active_storage_blob) do
|
39
|
+
prepend RailsExtend::ActiveStorage::BlobPrepend
|
40
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsExtend::ActiveStorage
|
4
|
+
module Variant
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
attribute :variation_digest, :string, null: false
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
ActiveSupport.on_load(:active_storage_blob) do
|
15
|
+
ActiveStorage::VariantRecord.include RailsExtend::ActiveStorage::Variant
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/configurable'
|
4
|
+
|
5
|
+
module RailsExtend #:nodoc:
|
6
|
+
include ActiveSupport::Configurable
|
7
|
+
|
8
|
+
configure do |config|
|
9
|
+
config.enum_key = ->(o, attribute){ "#{o.i18n_scope}.enum.#{o.base_class.model_name.i18n_key}.#{attribute}" }
|
10
|
+
config.help_key = ->(o, attribute){ "#{o.i18n_scope}.help.#{o.base_class.model_name.i18n_key}.#{attribute}" }
|
11
|
+
config.ignore_models = []
|
12
|
+
config.quiet_logs = [
|
13
|
+
'/rails/active_storage',
|
14
|
+
'/images',
|
15
|
+
'/@fs'
|
16
|
+
]
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Array
|
4
|
+
|
5
|
+
# arr = [1, 2, 3]
|
6
|
+
# arr.ljust! 5, nil
|
7
|
+
# # => [nil, nil, 1, 2, 3]
|
8
|
+
def ljust!(n, x)
|
9
|
+
return self if n < length
|
10
|
+
insert(0, *Array.new([0, n-length].max, x))
|
11
|
+
end
|
12
|
+
|
13
|
+
# fill an array with the given elements left;
|
14
|
+
# arr = [1, 2, 3]
|
15
|
+
# arr.rjust! 5, nil
|
16
|
+
# # => [1, 2, 3, nil, nil]
|
17
|
+
def rjust!(n, x)
|
18
|
+
return self if n < length
|
19
|
+
fill(x, length...n)
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# arr = [1, 2, 3]
|
24
|
+
# arr.mjust!(5, nil)
|
25
|
+
# # => [1, 2, nil, nil, 3]
|
26
|
+
def mjust!(n, x)
|
27
|
+
return self if n < length
|
28
|
+
insert((length / 2.0).ceil, *Array.new(n - length, x))
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# combine the same key hash like array
|
33
|
+
# raw_data = [
|
34
|
+
# { a: 1 },
|
35
|
+
# { a: 2 },
|
36
|
+
# { b: 2 },
|
37
|
+
# { b: 2 }
|
38
|
+
# ]
|
39
|
+
# raw_data.to_combine_h
|
40
|
+
# #=> { a: [1, 2], b: 2 }
|
41
|
+
def to_combine_h
|
42
|
+
self.inject({}) do |memo, obj|
|
43
|
+
memo.merge(obj) do |_, old_val, new_val|
|
44
|
+
v = (Array(old_val) + Array(new_val)).uniq
|
45
|
+
v.size > 1 ? v : v[0]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# raw_data = [
|
52
|
+
# [:a, 1],
|
53
|
+
# [:a, 2, 3],
|
54
|
+
# [:b, 2]
|
55
|
+
# ]
|
56
|
+
# raw_data.to_array_h
|
57
|
+
# #=> [ { a: 1 }, { a: 2 }, { b: 2 } ]
|
58
|
+
def to_array_h
|
59
|
+
self.map { |x, y| { x => y } }
|
60
|
+
end
|
61
|
+
|
62
|
+
# 2D array to csv file
|
63
|
+
# data = [
|
64
|
+
# [1, 2],
|
65
|
+
# [3, 4]
|
66
|
+
# ]
|
67
|
+
# data.to_csv_file
|
68
|
+
def to_csv_file(file = 'export.csv')
|
69
|
+
CSV.open(file, 'w') do |csv|
|
70
|
+
self.each { |ar| csv << ar }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# 比较两个数组忽略排序的情况下是否相等
|
75
|
+
def compare(other)
|
76
|
+
(self - other).empty? && (other - self).empty?
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Date
|
2
|
+
|
3
|
+
# Chinese custom after date
|
4
|
+
# '2018-01-01'.to_date.contract_after(2.month) => '2018-02-31'
|
5
|
+
# '2018-01-31'.to_date.contract_after(1.month) => '2018-02-28'
|
6
|
+
def contract_after(*afters, less: true)
|
7
|
+
parts = {}
|
8
|
+
# todo use inject or something
|
9
|
+
afters.each do |after|
|
10
|
+
parts.merge! after.parts
|
11
|
+
end
|
12
|
+
r = ActiveSupport::Duration.new(0, parts).after(self)
|
13
|
+
|
14
|
+
return r unless less
|
15
|
+
|
16
|
+
# if result day less than day, so
|
17
|
+
if (parts.keys & [:months, :years]).present?
|
18
|
+
r.day < day ? r : r - 1
|
19
|
+
else
|
20
|
+
r
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def parts
|
25
|
+
{
|
26
|
+
year: year,
|
27
|
+
month: month,
|
28
|
+
day: day
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class ActiveSupport::Duration
|
2
|
+
|
3
|
+
def inspect
|
4
|
+
return "#{value} #{I18n.t('duration.seconds')}" if @parts.empty?
|
5
|
+
|
6
|
+
@parts.
|
7
|
+
sort_by { |unit, _ | PARTS.index(unit) }.
|
8
|
+
map { |unit, val| "#{val} #{val == 1 ? I18n.t('duration')[unit] : I18n.t('durations')[unit]}" }.
|
9
|
+
to_sentence(locale: I18n.locale)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
def toggle(remove = true, other_hash)
|
4
|
+
dup.toggle!(remove, other_hash)
|
5
|
+
end
|
6
|
+
|
7
|
+
# a = {a: 1}
|
8
|
+
# a.toggle! a: 2
|
9
|
+
# => { a: [1, 2] }
|
10
|
+
# a.toggle! a: 3
|
11
|
+
# => { a: [1, 2, 3] }
|
12
|
+
def toggle!(remove = true, other_hash)
|
13
|
+
common_keys = self.keys & other_hash.keys
|
14
|
+
common_keys.each do |key|
|
15
|
+
if remove
|
16
|
+
removed = Array(self[key]) & Array(other_hash[key])
|
17
|
+
else
|
18
|
+
removed = []
|
19
|
+
end
|
20
|
+
added = Array(other_hash[key]) - Array(self[key])
|
21
|
+
self[key] = Array(self[key]) - removed + added
|
22
|
+
if self[key].empty?
|
23
|
+
self.delete(key)
|
24
|
+
elsif self[key].size == 1
|
25
|
+
self[key] = self[key][0]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
other_hash.except! *common_keys
|
29
|
+
self.merge! other_hash
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def diff_toggle(remove = true, other_hash)
|
34
|
+
removed = {}
|
35
|
+
added = {}
|
36
|
+
o = other_hash.extract!(*(self.keys & other_hash.keys))
|
37
|
+
|
38
|
+
if remove
|
39
|
+
removed.merge! o.common_basic(self)
|
40
|
+
end
|
41
|
+
added.merge! o.diff_remove(self)
|
42
|
+
added.merge! other_hash
|
43
|
+
|
44
|
+
[removed, added]
|
45
|
+
end
|
46
|
+
|
47
|
+
# a = { a:1, b: 2 }
|
48
|
+
# a.diff_remove { a: [1,2] }
|
49
|
+
# => removes: { b: 2 }
|
50
|
+
def diff_remove(other_hash, diff = {})
|
51
|
+
(keys | other_hash.keys).each do |key|
|
52
|
+
if self[key] != other_hash[key]
|
53
|
+
if self[key].is_a?(Hash) && other_hash[key].is_a?(Hash)
|
54
|
+
diff[key] = self[key].diff_remove(other_hash[key])
|
55
|
+
elsif self[key].is_a?(Array) && other_hash[key].is_a?(Array)
|
56
|
+
diff[key] = self[key] - other_hash[key]
|
57
|
+
elsif other_hash[key].is_a?(NilClass)
|
58
|
+
diff[key] = self[key]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
diff
|
64
|
+
end
|
65
|
+
|
66
|
+
# a = { a:1, b: 2 }
|
67
|
+
# a.diff_add { a: [1,2] }
|
68
|
+
# => adds: { b: 2 }
|
69
|
+
def diff_add(other_hash)
|
70
|
+
other_hash.diff_remove(self)
|
71
|
+
end
|
72
|
+
|
73
|
+
def diff_changes(other_hash)
|
74
|
+
[diff_remove(other_hash), diff_add(other_hash)]
|
75
|
+
end
|
76
|
+
|
77
|
+
def common_basic(h = {}, other_hash)
|
78
|
+
each do |key, value|
|
79
|
+
v = Array(value) & Array(other_hash[key])
|
80
|
+
if v.size > 1
|
81
|
+
h[key] = v
|
82
|
+
elsif v.size == 1
|
83
|
+
h[key] = v[0]
|
84
|
+
elsif v.empty?
|
85
|
+
h.delete(key)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
h
|
89
|
+
end
|
90
|
+
|
91
|
+
# 遍历出一个 hash 的根节点
|
92
|
+
def leaves
|
93
|
+
r = []
|
94
|
+
|
95
|
+
values.each do |value|
|
96
|
+
if value.is_a?(Hash)
|
97
|
+
r += value.leaves
|
98
|
+
else
|
99
|
+
r << value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
r
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|