easy_tools 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 68fe26d2e704b26fafef4dbf80b51d02f9fb23d7b6f2294d957192e725d82add
4
+ data.tar.gz: af7447ee4aae049bb9f9aa6e9ca8b73af9c9ed3a6d345e7b89d324f0d9121245
5
+ SHA512:
6
+ metadata.gz: ba25b53e0d8be84825f03afae7deb5b2755027a57e08b2d036a538d1feefb1b2f1dc581b2982f2b5d6d63dc97f38941b9b6b534ade7efe66e3e0741f63a150f6
7
+ data.tar.gz: 38433117b123422f6a0e05c0a390f9035502954487e525308392a1052b10038586176a2f6838a808bf84a13ae8539033f8b7d886763e2e5238ac0ba7e2e5a500
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.3
7
+ before_install: gem install bundler -v 1.17.2
@@ -0,0 +1,5 @@
1
+ {
2
+ "recommendations": [
3
+ "pavlitsky.yard"
4
+ ]
5
+ }
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at andy.you@rccchina.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 andy.you
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ <!--
2
+ * @Author: andy.you andy.you@rccchina.com
3
+ * @Date: 2025-08-20 13:42:05
4
+ * @LastEditors: andy.you andy.you@rccchina.com
5
+ * @LastEditTime: 2025-08-20 13:44:27
6
+ * @FilePath: /easy_tools/README.md
7
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
8
+ -->
9
+ # EasyTools
10
+
11
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/easy_tools`. To experiment with that code, run `bin/console` for an interactive prompt.
12
+
13
+ 一些简单的辅助工具,帮助你减少一些重复的代码
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'easy_tools'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install easy_tools
30
+
31
+ ## Usage
32
+
33
+ TODO: Write usage instructions here
34
+
35
+ ## Development
36
+
37
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
38
+
39
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
40
+
41
+ ## Contributing
42
+
43
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/easy_tools. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
44
+
45
+ ## License
46
+
47
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
48
+
49
+ ## Code of Conduct
50
+
51
+ Everyone interacting in the EasyTools project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/easy_tools/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,46 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'easy_tools/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'easy_tools'
7
+ spec.version = EasyTools::VERSION
8
+ spec.authors = ['andy.you']
9
+ spec.email = ['andy.you@rccchina.com']
10
+
11
+ spec.summary = 'EasyTools is a collection of utility methods and classes for Ruby on Rails applications.'
12
+ spec.description = 'EasyTools provides a set of helper methods and classes to simplify common tasks in Ruby on Rails applications, enhancing productivity and code maintainability.'
13
+ spec.homepage = 'https://git.rccchina.com/leads-in/omnicrm'
14
+ spec.license = 'MIT'
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
20
+
21
+ spec.metadata['homepage_uri'] = spec.homepage
22
+ spec.metadata['source_code_uri'] = 'https://git.rccchina.com/leads-in/omnicrm'
23
+ spec.metadata['changelog_uri'] = 'https://git.rccchina.com/leads-in/omnicrm'
24
+ else
25
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
26
+ 'public gem pushes.'
27
+ end
28
+
29
+ # Specify which files should be added to the gem when it is released.
30
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
31
+ spec.files = Dir.chdir(__dir__) do
32
+ `git ls-files -z`.split("\x0").reject do |f|
33
+ (File.expand_path(f) == __FILE__) ||
34
+ # 过滤一些不希望打包进gem的文件
35
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile .rubocop.yml])
36
+ end
37
+ end
38
+
39
+ spec.bindir = 'exe'
40
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
41
+ spec.require_paths = ['lib']
42
+
43
+ spec.add_development_dependency 'bundler', '~> 1.17'
44
+ # spec.add_development_dependency 'rake', '~> 10.0'
45
+ # spec.add_development_dependency 'rspec', '~> 3.0'
46
+ end
data/lib/auto_load.rb ADDED
@@ -0,0 +1,45 @@
1
+ # 自动加载
2
+ class AutoLoad
3
+ def self.define_autoloads(base_dir, namespace)
4
+ base_dir = File.expand_path(base_dir)
5
+
6
+ Dir.glob("#{base_dir}/**/*.rb").each do |file|
7
+ # 获取相对路径并移除扩展名
8
+ relative_path = file.sub("#{base_dir}/", '').sub('.rb', '')
9
+ path_parts = relative_path.split('/')
10
+
11
+ # 最后一个部分是类名
12
+ class_name = camelize(path_parts.pop)
13
+ # 前面的部分是模块路径
14
+ module_parts = path_parts.map { |part| camelize(part) }
15
+
16
+ # 找到或创建父模块
17
+ parent_module = find_or_create_module(module_parts, namespace)
18
+
19
+ # 设置 autoload(必须是符号)
20
+ parent_module.autoload class_name.to_sym, file
21
+ end
22
+ end
23
+
24
+ def self.find_or_create_module(module_parts, base_module)
25
+ current_module = base_module
26
+
27
+ module_parts.each do |mod_name|
28
+ mod_symbol = mod_name.to_sym
29
+
30
+ if current_module.const_defined?(mod_symbol)
31
+ current_module = current_module.const_get(mod_symbol)
32
+ else
33
+ new_module = Module.new
34
+ current_module.const_set(mod_symbol, new_module)
35
+ current_module = new_module
36
+ end
37
+ end
38
+
39
+ current_module
40
+ end
41
+
42
+ def self.camelize(string)
43
+ string.split('_').map(&:capitalize).join
44
+ end
45
+ end
@@ -0,0 +1,42 @@
1
+ module EasyTools
2
+ # 批量处理数据
3
+ class BatchManager
4
+ attr_accessor :is_wait, :interval, :max_thread
5
+
6
+ def initialize(data)
7
+ @data = data
8
+ @size = data.size
9
+ # 最大线程数不能超过数据库连接数
10
+ # 可能会取不到线程数,默认为1
11
+ @max_thread = ActiveRecord::Base.connection_pool.db_config.pool rescue 1
12
+ # 每根线程处理的业务数量
13
+ @interval = 100
14
+ # 是否等待结束
15
+ @is_wait = false
16
+ # 线程数组
17
+ @threads = []
18
+ end
19
+
20
+ #
21
+ # 启用多线程处理
22
+ #
23
+ def do_action
24
+ step = @size < @interval ? @size : @interval
25
+ start = 0
26
+ (start...@size).step(step).each_with_index.map do |index, _i|
27
+ end_step = step + index
28
+ @threads << Thread.new(@data[start...end_step]) do |data|
29
+ append_thread_function
30
+ yield data
31
+ end
32
+ start = end_step
33
+ # 线程大于等于线程数,阻塞等待
34
+ @threads.map(&:join) if @threads.size >= @max_thread
35
+ end
36
+ @threads.map(&:join) if @is_wait
37
+ end
38
+
39
+ # 增加线程处理中统一的方法
40
+ def append_thread_function; end
41
+ end
42
+ end
@@ -0,0 +1,53 @@
1
+ module EasyTools
2
+ module Constant
3
+ # 一分钟的秒数
4
+ SECONDS_IN_MINUTE = 60
5
+ # 一小时的秒数
6
+ SECONDS_IN_HOUR = 60 * 60
7
+ # 一天的秒数
8
+ SECONDS_IN_DAY = 60 * 60 * 24
9
+ # 一周的秒数
10
+ SECONDS_IN_WEEK = 60 * 60 * 24 * 7
11
+ # 30天的秒数
12
+ SECONDS_IN_MONTH = 60 * 60 * 24 * 30 # 按30天计算
13
+ # 365天的秒数
14
+ SECONDS_IN_YEAR = 60 * 60 * 24 * 365 # 按365天计算
15
+ # 邮箱 正则表达式
16
+ EMAIL_REGULAR_EXPRESSION = /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/.freeze
17
+ # 手机号 正则表达式
18
+ MOBILE_REGULAR_EXPRESSION = /^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/.freeze
19
+ # 电话 正则表达式
20
+ PHONE_REGULAR_EXPRESSION = /\d{3}-\d{8}|\d{4}-\d{7}/.freeze
21
+ # 网址/链接正则
22
+ LINK_REGULAR_EXPRESSION = %r{^((ht|f)tps?)://[\w-]+(\.[\w-]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?$}.freeze
23
+
24
+ # 支持的所有日期时间格式正则表达式
25
+ DATETIME_FORMATS = {
26
+ # 基本日期格式(支持个位数)
27
+ yyyymmdd: /\A\d{8}\z/, # 20231225
28
+ yyyy_mm_dd: /\A\d{4}-\d{1,2}-\d{1,2}\z/, # 2023-12-25 或 2023-1-5
29
+ yyyy_slash_mm_dd: %r{\A\d{4}/\d{1,2}/\d{1,2}\z}, # 2023/12/25 或 2023/1/5
30
+ yy_mm_dd: /\A\d{2}-\d{1,2}-\d{1,2}\z/, # 23-12-25 或 23-1-5
31
+ yy_slash_mm_dd: %r{\A\d{2}/\d{1,2}/\d{1,2}\z}, # 23/12/25 或 23/1/5
32
+ yyyy_dot_mm_dd: /\A\d{4}\.\d{1,2}\.\d{1,2}\z/, # 2023.12.25 或 2023.1.5
33
+ yy_dot_mm_dd: /\A\d{2}\.\d{1,2}\.\d{1,2}\z/, # 23.12.25 或 23.1.5
34
+
35
+ # 中文日期格式(支持个位数)
36
+ chinese_date: /\A\d{4}年\d{1,2}月\d{1,2}日\z/, # 2023年12月25日 或 2023年1月5日
37
+ chinese_short_date: /\A\d{2}年\d{1,2}月\d{1,2}日\z/, # 23年12月25日 或 23年1月5日
38
+
39
+ # 日期时间格式(支持个位数)
40
+ yyyymmdd_hhmmss: /\A\d{8} \d{1,2}:\d{1,2}:\d{1,2}\z/, # 20231225 14:30:45 或 20231225 4:3:5
41
+ yyyy_mm_dd_hhmmss: /\A\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}\z/, # 2023-12-25 14:30:45 或 2023-1-5 4:3:5
42
+ yyyy_slash_mm_dd_hhmmss: %r{\A\d{4}/\d{1,2}/\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}\z}, # 2023/12/25 14:30:45 或 2023/1/5 4:3:5
43
+ iso_datetime: /\A\d{4}-\d{1,2}-\d{1,2}T\d{1,2}:\d{1,2}:\d{1,2}\z/, # 2023-12-25T14:30:45 或 2023-1-5T4:3:5
44
+
45
+ # 中文日期时间格式(支持个位数)
46
+ chinese_datetime: /\A\d{4}年\d{1,2}月\d{1,2}日\s\d{1,2}时\d{1,2}分\d{1,2}秒\z/, # 2023年12月25日 14时30分45秒 或 2023年1月5日 4时3分5秒
47
+ chinese_datetime_short: /\A\d{2}年\d{1,2}月\d{1,2}日\s\d{1,2}时\d{1,2}分\d{1,2}秒\z/, # 23年12月25日 14时30分45秒 或 23年1月5日 4时3分5秒
48
+
49
+ # 纯时间格式(支持个位数)
50
+ time_only: /\A\d{1,2}:\d{1,2}:\d{1,2}\z/, # 14:30:45 或 4:3:5
51
+ }.freeze
52
+ end
53
+ end
@@ -0,0 +1,57 @@
1
+ module EasyTools
2
+ # EasyTools is a collection of utility methods and classes for Ruby on Rails applications.
3
+ class Core
4
+ class << self
5
+ # 列表转树结构
6
+ # @params data Hash 列表数据
7
+ # 数据结构
8
+ # [{
9
+ # id: 1, # 主键id
10
+ # parent_id: nil, # 父id
11
+ # order: 0, # 排序
12
+ # body: {title: '测试'}, # 主体内容
13
+ # },...]
14
+ # @return [Array] [{:title=>'测试', :children=>[]}]
15
+ #
16
+ def list_to_tree(data, parent_id = nil, sort_by: [:order])
17
+ nodes = data.select { |item| item[:parent_id] == parent_id }
18
+ # 多字段排序
19
+ nodes.sort_by! { |node| sort_by.map { |field| node[field] } }
20
+
21
+ nodes.map do |node|
22
+ {
23
+ **node[:body],
24
+ children: list_to_tree(data, node[:id], sort_by: sort_by),
25
+ }
26
+ end
27
+ end
28
+
29
+ # 循环查询全部数据
30
+ # @params page 开始页数
31
+ # @params per_apge 每条分页数据
32
+ def find_all(page: 1, per_page: 1000, max_size: 10_000, no_safe: false)
33
+ data = []
34
+ loop do
35
+ current_data = yield(page, per_page)
36
+ data.concat(current_data)
37
+ # 数据已经取完了,则跳出
38
+ break if per_page > current_data.size
39
+
40
+ # 10w条数据需要提示中止
41
+ if data.size > max_size && !no_safe
42
+ raise EasyTools::Error, '循环获取的数量过大,如果需要获取更多的数据可以调整 max_size参数或开启no_safe: true'
43
+ end
44
+
45
+ page += 1
46
+ end
47
+ data
48
+ end
49
+
50
+ # 创建自动嵌套的 Hash
51
+ # @return Hash
52
+ def auto_hash
53
+ Hash.new { |h, k| h[k] = auto_hash }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,73 @@
1
+ require 'time'
2
+ module EasyTools
3
+ # 时间处理类
4
+ class DateTime
5
+ extend EasyTools::DateTimeExt::Validator
6
+
7
+ class << self
8
+ # 安全转成时间格式
9
+ # @params input [Integer|String] 输入参数【时间戳|时间格式的字符串】
10
+ # @return DateTime 返回转换后的时间类型
11
+ def to_time(input)
12
+ case input
13
+ when Integer
14
+ # 处理毫秒级时间戳
15
+ input = input.to_s[0, 10].to_i
16
+ Time.at(input)
17
+ when String
18
+ raise EasyTools::Error, '请输入有效的日期时间格式' unless valid?(input)
19
+
20
+ Time.parse(normalize(input))
21
+ when DateTime, Date, Time
22
+ input
23
+ else
24
+ raise EasyTools::Error, '暂不支持此类型'
25
+ end
26
+ end
27
+
28
+ # 转成时间格式字符串
29
+ #
30
+ # @param input [String, Numeric, Time] 时间输入,可以是日期字符串、时间戳或Time对象
31
+ # @param format [String] 格式化时间字符串,同strftime方法参数
32
+ # @return [String] 格式化后日期
33
+ def strftime(input, format = '%Y-%m-%d %H:%M:%S')
34
+ time = to_time(input)
35
+ time.strftime(format)
36
+ end
37
+
38
+ # 时间转换方法:将时间转换为相对时间描述(如:几分钟前)
39
+ #
40
+ # @param input [String, Numeric, Time] 时间输入,可以是日期字符串、时间戳或Time对象
41
+ # @return [String] 相对时间描述或格式化日期
42
+ def format_relative_time(input)
43
+ time = input.is_a?(Time) ? input : to_time(input)
44
+ # 计算时间差
45
+ time_difference = (Time.now - time).to_i
46
+ # 根据时间差返回相应的描述
47
+ format_time_description(time_difference)
48
+ end
49
+
50
+ # 格式化时间描述
51
+ # @param difference [Integer] 秒数的差值
52
+ # @return [String] 格式化时间描述
53
+ def format_time_description(difference)
54
+ case difference
55
+ when 0...Constant::SECONDS_IN_MINUTE
56
+ "#{difference}秒前"
57
+ when Constant::SECONDS_IN_MINUTE...Constant::SECONDS_IN_HOUR
58
+ "#{difference / Constant::SECONDS_IN_MINUTE}分钟前"
59
+ when Constant::SECONDS_IN_HOUR...Constant::SECONDS_IN_DAY
60
+ "#{difference / Constant::SECONDS_IN_HOUR}小时前"
61
+ when Constant::SECONDS_IN_DAY...Constant::SECONDS_IN_WEEK
62
+ "#{difference / Constant::SECONDS_IN_DAY}天前"
63
+ when Constant::SECONDS_IN_WEEK...Constant::SECONDS_IN_MONTH
64
+ "#{difference / Constant::SECONDS_IN_WEEK}周前" # 修复:除以周的秒数
65
+ when Constant::SECONDS_IN_MONTH...Constant::SECONDS_IN_YEAR
66
+ "#{difference / Constant::SECONDS_IN_MONTH}个月前"
67
+ else
68
+ "#{difference / Constant::SECONDS_IN_YEAR}年前"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,362 @@
1
+ require 'date'
2
+
3
+ module EasyTools
4
+ module DateTimeExt
5
+ # DateTime 扩展模块,提供日期时间格式验证和转换功能
6
+ module Validator
7
+ # 验证日期时间字符串是否符合支持的格式
8
+ #
9
+ # @param datetime_string [String] 要验证的日期时间字符串
10
+ # @return [Boolean] 如果字符串符合任一支持的格式且日期时间组件有效,返回 true;否则返回 false
11
+ #
12
+ # @example
13
+ # valid?("2023-12-25") #=> true
14
+ # valid?("2023.1.1") #=> true
15
+ # valid?("invalid") #=> false
16
+ def valid?(datetime_string)
17
+ return false unless datetime_string.is_a?(String)
18
+
19
+ # 检查是否符合任何一种格式
20
+ ::EasyTools::Constant::DATETIME_FORMATS.any? do |format_name, regex|
21
+ datetime_string.match?(regex) && valid_datetime_components?(datetime_string, format_name)
22
+ end
23
+ end
24
+
25
+ # 验证日期时间字符串的各个组件是否有效
26
+ #
27
+ # @param datetime_string [String] 日期时间字符串
28
+ # @param format_name [Symbol] 格式名称
29
+ # @return [Boolean] 如果所有组件都有效返回 true;否则返回 false
30
+ #
31
+ # @example
32
+ # valid_datetime_components?("2023-13-01", :yyyy_mm_dd) #=> false (无效月份)
33
+ # valid_datetime_components?("2023.1.1", :yyyy_dot_mm_dd) #=> true
34
+ def valid_datetime_components?(datetime_string, format_name)
35
+ components = extract_components(datetime_string, format_name)
36
+ return false unless components
37
+
38
+ year, month, day, hour, minute, second = components
39
+
40
+ # 校验日期部分
41
+ if year && month && day && !valid_date?(year, month, day)
42
+ return false
43
+ end
44
+
45
+ # 校验时间部分
46
+ if (hour || minute || second) && !valid_time?(hour, minute, second)
47
+ return false
48
+ end
49
+
50
+ true
51
+ end
52
+
53
+ # 从日期时间字符串中提取各个组件
54
+ #
55
+ # @param datetime_string [String] 日期时间字符串
56
+ # @param format_name [Symbol] 格式名称
57
+ # @return [Array<Integer, Integer, Integer, Integer, Integer, Integer>]
58
+ # 返回 [年, 月, 日, 时, 分, 秒] 数组,不存在的组件为 nil
59
+ # @return [nil] 如果格式不匹配返回 nil
60
+ #
61
+ # @example
62
+ # extract_components("2023-12-25", :yyyy_mm_dd) #=> [2023, 12, 25, nil, nil, nil]
63
+ # extract_components("2023.1.5", :yyyy_dot_mm_dd) #=> [2023, 1, 5, nil, nil, nil]
64
+ def extract_components(datetime_string, format_name)
65
+ case format_name
66
+ when :yyyymmdd
67
+ [datetime_string[0..3].to_i, datetime_string[4..5].to_i, datetime_string[6..7].to_i, nil, nil, nil]
68
+
69
+ when :yyyy_mm_dd, :yyyy_slash_mm_dd, :yyyy_dot_mm_dd
70
+ separator = get_separator(datetime_string)
71
+ parts = datetime_string.split(separator)
72
+ year = parts[0].to_i
73
+ month = parts[1].to_i
74
+ day = parts[2].to_i
75
+ [year, month, day, nil, nil, nil]
76
+
77
+ when :yy_mm_dd, :yy_slash_mm_dd, :yy_dot_mm_dd
78
+ separator = get_separator(datetime_string)
79
+ parts = datetime_string.split(separator)
80
+ year_str = parts[0].to_i
81
+ month = parts[1].to_i
82
+ day = parts[2].to_i
83
+ year = convert_two_digit_year(year_str)
84
+ [year, month, day, nil, nil, nil]
85
+
86
+ when :chinese_date
87
+ match = datetime_string.match(/\A(\d{4})年(\d{1,2})月(\d{1,2})日\z/)
88
+ [match[1].to_i, match[2].to_i, match[3].to_i, nil, nil, nil] if match
89
+
90
+ when :chinese_short_date
91
+ match = datetime_string.match(/\A(\d{2})年(\d{1,2})月(\d{1,2})日\z/)
92
+ year = convert_two_digit_year(match[1].to_i) if match
93
+ [year, match[2].to_i, match[3].to_i, nil, nil, nil] if match
94
+
95
+ when :yyyymmdd_hhmmss
96
+ date_part, time_part = datetime_string.split(' ')
97
+ hour, minute, second = time_part.split(':').map(&:to_i)
98
+ [date_part[0..3].to_i, date_part[4..5].to_i, date_part[6..7].to_i, hour, minute, second]
99
+
100
+ when :yyyy_mm_dd_hhmmss, :yyyy_slash_mm_dd_hhmmss
101
+ date_part, time_part = datetime_string.split(' ')
102
+ separator = get_separator(date_part)
103
+ parts = date_part.split(separator)
104
+ year = parts[0].to_i
105
+ month = parts[1].to_i
106
+ day = parts[2].to_i
107
+ hour, minute, second = time_part.split(':').map(&:to_i)
108
+ [year, month, day, hour, minute, second]
109
+
110
+ when :iso_datetime
111
+ date_part, time_part = datetime_string.split('T')
112
+ year, month, day = date_part.split('-').map(&:to_i)
113
+ hour, minute, second = time_part.split(':').map(&:to_i)
114
+ [year, month, day, hour, minute, second]
115
+
116
+ when :chinese_datetime
117
+ match = datetime_string.match(/\A(\d{4})年(\d{1,2})月(\d{1,2})日\s(\d{1,2})时(\d{1,2})分(\d{1,2})秒\z/)
118
+ [match[1].to_i, match[2].to_i, match[3].to_i, match[4].to_i, match[5].to_i, match[6].to_i] if match
119
+
120
+ when :chinese_datetime_short
121
+ match = datetime_string.match(/\A(\d{2})年(\d{1,2})月(\d{1,2})日\s(\d{1,2})时(\d{1,2})分(\d{1,2})秒\z/)
122
+ year = convert_two_digit_year(match[1].to_i) if match
123
+ [year, match[2].to_i, match[3].to_i, match[4].to_i, match[5].to_i, match[6].to_i] if match
124
+
125
+ when :time_only
126
+ hour, minute, second = datetime_string.split(':').map(&:to_i)
127
+ [nil, nil, nil, hour, minute, second]
128
+ end
129
+ end
130
+
131
+ # 获取字符串中的分隔符
132
+ #
133
+ # @param string [String] 包含分隔符的字符串
134
+ # @return [String] 分隔符字符('-', '/', '.' 或空字符串)
135
+ #
136
+ # @example
137
+ # get_separator("2023-12-25") #=> "-"
138
+ # get_separator("2023/12/25") #=> "/"
139
+ # get_separator("2023.1.1") #=> "."
140
+ def get_separator(string)
141
+ if string.include?('-')
142
+ '-'
143
+ elsif string.include?('/')
144
+ '/'
145
+ elsif string.include?('.')
146
+ '.'
147
+ else
148
+ ''
149
+ end
150
+ end
151
+
152
+ # 将两位年份转换为四位年份
153
+ #
154
+ # 转换规则:00-79 转换为 2000-2079,80-99 转换为 1980-1999
155
+ #
156
+ # @param year [Integer] 两位年份
157
+ # @return [Integer] 四位年份
158
+ #
159
+ # @example
160
+ # convert_two_digit_year(23) #=> 2023
161
+ # convert_two_digit_year(85) #=> 1985
162
+ def convert_two_digit_year(year)
163
+ year < 80 ? year + 2000 : year + 1900
164
+ end
165
+
166
+ # 验证日期组件的有效性
167
+ #
168
+ # @param year [Integer] 年份
169
+ # @param month [Integer] 月份
170
+ # @param day [Integer] 日期
171
+ # @return [Boolean] 如果日期有效返回 true;否则返回 false
172
+ #
173
+ # @example
174
+ # valid_date?(2023, 2, 29) #=> false (2023年不是闰年)
175
+ # valid_date?(2024, 2, 29) #=> true (2024年是闰年)
176
+ # valid_date?(2023, 1, 1) #=> true
177
+ def valid_date?(year, month, day)
178
+ return false unless month.between?(1, 12)
179
+ return false unless day.between?(1, days_in_month(year, month))
180
+
181
+ true
182
+ end
183
+
184
+ # 验证时间组件的有效性
185
+ #
186
+ # @param hour [Integer, nil] 小时
187
+ # @param minute [Integer, nil] 分钟
188
+ # @param second [Integer, nil] 秒
189
+ # @return [Boolean] 如果时间有效返回 true;否则返回 false
190
+ #
191
+ # @example
192
+ # valid_time?(25, 30, 45) #=> false (小时无效)
193
+ # valid_time?(14, 30, 45) #=> true
194
+ # valid_time?(4, 3, 5) #=> true
195
+ def valid_time?(hour, minute, second)
196
+ # 如果提供了时间部分,则验证所有时间组件
197
+ if hour || minute || second
198
+ if hour && !hour.between?(0, 23)
199
+ return false
200
+ end
201
+ if minute && !minute.between?(0, 59)
202
+ return false
203
+ end
204
+ if second && !second.between?(0, 59)
205
+ return false
206
+ end
207
+ end
208
+ true
209
+ end
210
+
211
+ # 获取指定年份和月份的天数
212
+ #
213
+ # @param year [Integer] 年份
214
+ # @param month [Integer] 月份
215
+ # @return [Integer] 该月的天数
216
+ #
217
+ # @example
218
+ # days_in_month(2023, 2) #=> 28
219
+ # days_in_month(2024, 2) #=> 29
220
+ def days_in_month(year, month)
221
+ case month
222
+ when 2
223
+ leap_year?(year) ? 29 : 28
224
+ when 4, 6, 9, 11
225
+ 30
226
+ else
227
+ 31
228
+ end
229
+ end
230
+
231
+ # 判断指定年份是否为闰年
232
+ #
233
+ # @param year [Integer] 年份
234
+ # @return [Boolean] 如果是闰年返回 true;否则返回 false
235
+ #
236
+ # @example
237
+ # leap_year?(2023) #=> false
238
+ # leap_year?(2024) #=> true
239
+ def leap_year?(year)
240
+ ((year % 4).zero? && year % 100 != 0) || (year % 400).zero?
241
+ end
242
+
243
+ # 将日期时间字符串标准化为指定格式
244
+ #
245
+ # @param datetime_string [String] 日期时间字符串
246
+ # @param format [Symbol] 输出格式,可选值::iso, :chinese, :date_only, :time_only
247
+ # @return [String, nil] 标准化后的字符串,如果输入无效返回 nil
248
+ #
249
+ # @example
250
+ # normalize("20231225", format: :iso) #=> "2023-12-25"
251
+ # normalize("2023.1.1", format: :iso) #=> "2023-01-01"
252
+ # normalize("2023-12-25", format: :chinese) #=> "2023年12月25日"
253
+ def normalize(datetime_string, format: :iso)
254
+ return nil unless valid?(datetime_string)
255
+
256
+ components = extract_components_from_string(datetime_string)
257
+ return nil unless components
258
+
259
+ year, month, day, hour, minute, second = components
260
+
261
+ case format
262
+ when :iso
263
+ format_iso(year, month, day, hour, minute, second)
264
+ when :chinese
265
+ format_chinese(year, month, day, hour, minute, second)
266
+ when :date_only
267
+ format_date_only(year, month, day)
268
+ when :time_only
269
+ format_time_only(hour, minute, second)
270
+ else
271
+ format_iso(year, month, day, hour, minute, second)
272
+ end
273
+ end
274
+
275
+ # 从字符串中提取日期时间组件
276
+ #
277
+ # @param datetime_string [String] 日期时间字符串
278
+ # @return [Array<Integer>, nil] 返回组件数组或 nil
279
+ #
280
+ # @private
281
+ def extract_components_from_string(datetime_string)
282
+ format_name = ::EasyTools::Constant::DATETIME_FORMATS.find do |name, regex|
283
+ datetime_string.match?(regex) && valid_datetime_components?(datetime_string, name)
284
+ end&.first
285
+
286
+ extract_components(datetime_string, format_name) if format_name
287
+ end
288
+
289
+ # 格式化为 ISO 格式的日期时间字符串
290
+ #
291
+ # @param year [Integer, nil] 年份
292
+ # @param month [Integer, nil] 月份
293
+ # @param day [Integer, nil] 日期
294
+ # @param hour [Integer, nil] 小时
295
+ # @param minute [Integer, nil] 分钟
296
+ # @param second [Integer, nil] 秒
297
+ # @return [String, nil] 格式化后的字符串
298
+ #
299
+ # @private
300
+ def format_iso(year, month, day, hour, minute, second)
301
+ date_part = "#{year}-#{month.to_s.rjust(2, '0')}-#{day.to_s.rjust(2, '0')}" if year && month && day
302
+ time_part = "#{hour.to_s.rjust(2, '0')}:#{minute.to_s.rjust(2, '0')}:#{second.to_s.rjust(2, '0')}" if hour && minute && second
303
+
304
+ if date_part && time_part
305
+ "#{date_part} #{time_part}"
306
+ elsif date_part
307
+ date_part
308
+ elsif time_part
309
+ time_part
310
+ end
311
+ end
312
+
313
+ # 格式化为中文格式的日期时间字符串
314
+ #
315
+ # @param year [Integer, nil] 年份
316
+ # @param month [Integer, nil] 月份
317
+ # @param day [Integer, nil] 日期
318
+ # @param hour [Integer, nil] 小时
319
+ # @param minute [Integer, nil] 分钟
320
+ # @param second [Integer, nil] 秒
321
+ # @return [String, nil] 格式化后的字符串
322
+ #
323
+ # @private
324
+ def format_chinese(year, month, day, hour, minute, second)
325
+ date_part = "#{year}年#{month}月#{day}日" if year && month && day
326
+ time_part = "#{hour}时#{minute}分#{second}秒" if hour && minute && second
327
+
328
+ if date_part && time_part
329
+ "#{date_part}#{time_part}"
330
+ elsif date_part
331
+ date_part
332
+ elsif time_part
333
+ time_part
334
+ end
335
+ end
336
+
337
+ # 格式化为仅日期字符串
338
+ #
339
+ # @param year [Integer] 年份
340
+ # @param month [Integer] 月份
341
+ # @param day [Integer] 日期
342
+ # @return [String] 格式化后的日期字符串
343
+ #
344
+ # @private
345
+ def format_date_only(year, month, day)
346
+ "#{year}-#{month.to_s.rjust(2, '0')}-#{day.to_s.rjust(2, '0')}" if year && month && day
347
+ end
348
+
349
+ # 格式化为仅时间字符串
350
+ #
351
+ # @param hour [Integer] 小时
352
+ # @param minute [Integer] 分钟
353
+ # @param second [Integer] 秒
354
+ # @return [String] 格式化后的时间字符串
355
+ #
356
+ # @private
357
+ def format_time_only(hour, minute, second)
358
+ "#{hour.to_s.rjust(2, '0')}:#{minute.to_s.rjust(2, '0')}:#{second.to_s.rjust(2, '0')}" if hour && minute && second
359
+ end
360
+ end
361
+ end
362
+ end
@@ -0,0 +1,40 @@
1
+ require 'bigdecimal'
2
+
3
+ module EasyTools
4
+ # 数字方法
5
+ class Math
6
+ class << self
7
+ # @Notes: 精确除法并向上取整到指定小数位数
8
+ # @param dividend [Numeric, String] 被除数
9
+ # @param divisor [Numeric, String] 除数
10
+ # @param decimal_places [Integer] 保留的小数位数,默认为2
11
+ # @return [Float] 结果为向上取整后的浮点数
12
+ def precise_divide_and_ceil_percent(dividend, divisor, decimal_places = 2)
13
+ BigDecimal.mode(BigDecimal::ROUND_MODE, :up) # 设置向上取整模式
14
+ BigDecimal.limit(decimal_places + 4) # 设置足够精度
15
+
16
+ # 计算百分比(乘以100)
17
+ percent = (BigDecimal(dividend.to_s) / BigDecimal(divisor.to_s)) * 100
18
+
19
+ # 向上取整并格式化为指定小数位
20
+ ("%.#{decimal_places}f" % percent.ceil(decimal_places)).to_f
21
+ end
22
+
23
+ # @Notes: 精确除法并向上取整到指定小数位数
24
+ # @param dividend [Numeric, String] 被除数
25
+ # @param divisor [Numeric, String] 除数
26
+ # @param decimal_places [Integer] 保留的小数位数,默认为2
27
+ # @return [Float] 结果为向上取整后的浮点数
28
+ def precise_divide_and_round_percent(dividend, divisor, decimal_places = 2)
29
+ BigDecimal.mode(BigDecimal::ROUND_MODE, :half_up) # 设置向上取整模式
30
+ BigDecimal.limit(decimal_places + 4) # 设置足够精度
31
+
32
+ # 计算百分比(乘以100)
33
+ percent = (BigDecimal(dividend.to_s) / BigDecimal(divisor.to_s)) * 100
34
+
35
+ # 向上取整并格式化为指定小数位
36
+ ("%.#{decimal_places}f" % percent.round(decimal_places)).to_f
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,24 @@
1
+ module EasyTools
2
+ # EasyTools is a collection of utility methods and classes for Ruby on Rails applications.
3
+ class Rails
4
+ include EasyTools::RailsExt::Hash
5
+
6
+ def initialize
7
+ unless Object.const_defined?('ActiveSupport::HashWithIndifferentAccess')
8
+ raise EasyTools::Error,
9
+ 'ActiveSupport::HashWithIndifferentAccess is not available. Please ensure you have ActiveSupport loaded.'
10
+ end
11
+ end
12
+
13
+ # 不存在方法时调用
14
+ def self.method_missing(method_name, *args, **hash_data, &block)
15
+ this.send(method_name, *args, **hash_data, &block)
16
+ end
17
+
18
+ class << self
19
+ def this
20
+ @this ||= new
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ module EasyTools
2
+ module RailsExt
3
+ # 关于 Hash 的一些扩展方法
4
+ module Hash
5
+ # 对象转键值对
6
+ def to_kv(object, key, value = nil)
7
+ ActiveSupport::HashWithIndifferentAccess[object.map do |o|
8
+ if o.is_a?(Hash)
9
+ o = o.deep_symbolize_keys
10
+ [o[key.to_sym], value.nil? ? o : o[value.to_sym]]
11
+ else
12
+ [o.try(key.to_sym), value.nil? ? o : o.try(value.to_sym)]
13
+ end
14
+ end]
15
+ rescue StandardError
16
+ {}
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module EasyTools
2
+ VERSION = '0.1.0'.freeze
3
+ end
data/lib/easy_tools.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'auto_load'
2
+ AutoLoad.define_autoloads('lib/easy_tools', EasyTools)
3
+
4
+ # 小小的帮助工具集合
5
+ # 积累了实际生产场景中遇到问题的解决方法
6
+ # 工具的目标为不一定通用,但一定好用
7
+ module EasyTools
8
+ class Error < StandardError; end
9
+ # EasyTools is a collection of utility methods and classes for Ruby on Rails applications.
10
+ class << self
11
+ # Returns the version of the currently loaded EasyTools as a string.
12
+ def version
13
+ EasyTools::VERSION
14
+ end
15
+
16
+ # Provides a way to configure EasyTools.
17
+ #
18
+ # @yield [config] The configuration block.
19
+ # @yieldparam config [Configuration] The configuration object.
20
+ #
21
+ # @return [Configuration] The current configuration object.
22
+ def configure
23
+ yield(configuration)
24
+ end
25
+
26
+ # Returns the current configuration object.
27
+ def configuration
28
+ @configuration ||= Configuration.new
29
+ end
30
+
31
+ # 加载Rails相关的扩展
32
+ def rails
33
+ EasyTools::Rails.this
34
+ end
35
+ end
36
+
37
+ # 配置相关
38
+ class Configuration
39
+ def initialize
40
+ @logger = Logger.new($stdout)
41
+ end
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: easy_tools
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - andy.you
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-09-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.17'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.17'
27
+ description: EasyTools provides a set of helper methods and classes to simplify common
28
+ tasks in Ruby on Rails applications, enhancing productivity and code maintainability.
29
+ email:
30
+ - andy.you@rccchina.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".rspec"
36
+ - ".travis.yml"
37
+ - ".vscode/extensions.json"
38
+ - CODE_OF_CONDUCT.md
39
+ - LICENSE.txt
40
+ - README.md
41
+ - Rakefile
42
+ - easy_tools.gemspec
43
+ - lib/auto_load.rb
44
+ - lib/easy_tools.rb
45
+ - lib/easy_tools/batch_manager.rb
46
+ - lib/easy_tools/constant.rb
47
+ - lib/easy_tools/core.rb
48
+ - lib/easy_tools/date_time.rb
49
+ - lib/easy_tools/date_time_ext/validator.rb
50
+ - lib/easy_tools/math.rb
51
+ - lib/easy_tools/rails.rb
52
+ - lib/easy_tools/rails_ext/hash.rb
53
+ - lib/easy_tools/version.rb
54
+ homepage: https://git.rccchina.com/leads-in/omnicrm
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ allowed_push_host: https://rubygems.org
59
+ homepage_uri: https://git.rccchina.com/leads-in/omnicrm
60
+ source_code_uri: https://git.rccchina.com/leads-in/omnicrm
61
+ changelog_uri: https://git.rccchina.com/leads-in/omnicrm
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 3.0.3
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: EasyTools is a collection of utility methods and classes for Ruby on Rails
81
+ applications.
82
+ test_files: []