rails_extend 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|