diff --git a/.gitignore b/.gitignore
index c6d98d1..5ff6309 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,98 +1,38 @@
-# ---> JetBrains
-# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
-# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
-
-# User-specific stuff
-.idea/**/workspace.xml
-.idea/**/tasks.xml
-.idea/**/usage.statistics.xml
-.idea/**/dictionaries
-.idea/**/shelf
-
-# AWS User-specific
-.idea/**/aws.xml
-
-# Generated files
-.idea/**/contentModel.xml
-
-# Sensitive or high-churn files
-.idea/**/dataSources/
-.idea/**/dataSources.ids
-.idea/**/dataSources.local.xml
-.idea/**/sqlDataSources.xml
-.idea/**/dynamic.xml
-.idea/**/uiDesigner.xml
-.idea/**/dbnavigator.xml
-
-# Gradle
-.idea/**/gradle.xml
-.idea/**/libraries
-
-# Gradle and Maven with auto-import
-# When using Gradle or Maven with auto-import, you should exclude module files,
-# since they will be recreated, and may cause churn. Uncomment if using
-# auto-import.
-# .idea/artifacts
-# .idea/compiler.xml
-# .idea/jarRepositories.xml
-# .idea/modules.xml
-# .idea/*.iml
-# .idea/modules
-# *.iml
-# *.ipr
-
-# CMake
-cmake-build-*/
-
-# Mongo Explorer plugin
-.idea/**/mongoSettings.xml
-
-# File-based project format
-*.iws
-
-# IntelliJ
-out/
-
-# mpeltonen/sbt-idea plugin
-.idea_modules/
-
-# JIRA plugin
-atlassian-ide-plugin.xml
-
-# Cursive Clojure plugin
-.idea/replstate.xml
-
-# SonarLint plugin
-.idea/sonarlint/
-
-# Crashlytics plugin (for Android Studio and IntelliJ)
-com_crashlytics_export_strings.xml
-crashlytics.properties
-crashlytics-build.properties
-fabric.properties
-
-# Editor-based Rest Client
-.idea/httpRequests
-
-# Android studio 3.1+ serialized cache file
-.idea/caches/build_file_checksums.ser
-
-# ---> Maven
target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
-# https://github.com/takari/maven-wrapper#usage-without-binary-jar
-.mvn/wrapper/maven-wrapper.jar
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
-# Eclipse m2e generated files
-# Eclipse Core
-.project
-# JDT-specific (Eclipse Java Development Tools)
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/CopilotChatHistory.xml b/.idea/CopilotChatHistory.xml
new file mode 100644
index 0000000..183afa9
--- /dev/null
+++ b/.idea/CopilotChatHistory.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..aa00ffa
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..a0e1fc4
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100644
index 0000000..2b63946
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..549a174
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,37 @@
+
+
+ 4.0.0
+
+ com.imyeyu.utils
+ timi-utils
+ 0.0.1
+
+
+ 21
+ 21
+ UTF-8
+ true
+
+
+
+
+ com.imyeyu.java
+ timi-java
+ 0.0.1
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.2
+ test
+
+
+ org.projectlombok
+ lombok
+ 1.18.34
+ test
+
+
+
diff --git a/src/main/java/com/imyeyu/utils/AsciiTable.java b/src/main/java/com/imyeyu/utils/AsciiTable.java
new file mode 100644
index 0000000..cd9009b
--- /dev/null
+++ b/src/main/java/com/imyeyu/utils/AsciiTable.java
@@ -0,0 +1,250 @@
+package com.imyeyu.utils;
+
+import com.imyeyu.java.bean.CallbackArgReturn;
+import com.imyeyu.java.bean.timi.TimiCode;
+import com.imyeyu.java.bean.timi.TimiException;
+import com.imyeyu.java.ref.Ref;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author 夜雨
+ * @since 2024-12-18 10:10
+ */
+public class AsciiTable {
+
+ /** 制表符水平边框 */
+ private static final String BORDER_H = "─";
+
+ /** 制表符垂直边框 */
+ private static final String BORDER_V = "│";
+
+ /** 制表符左上边框 */
+ private static final String BORDER_TL = "┌";
+
+ /** 制表符上边框 */
+ private static final String BORDER_T = "┬";
+
+ /** 制表符右上边框 */
+ private static final String BORDER_TR = "┐";
+
+ /** 制表符左边框 */
+ private static final String BORDER_L = "├";
+
+ /** 制表符十字边框 */
+ private static final String BORDER_P = "┼";
+
+ /** 制表符右边框 */
+ private static final String BORDER_R = "┤";
+
+ /** 制表符左下边框 */
+ private static final String BORDER_BL = "└";
+
+ /** 制表符下边框 */
+ private static final String BORDER_B = "┴";
+
+ /** 制表符右下边框 */
+ private static final String BORDER_BR = "┘";
+
+ private final List> colMappingList = new ArrayList<>();
+
+ private final List rowList = new ArrayList<>();
+
+ public String render(List dataList) {
+ try {
+
+ {
+ // 表头
+ Row header = new Row();
+ for (int i = 0; i < colMappingList.size(); i++) {
+ Row.Col col = new Row.Col();
+ col.text = colMappingList.get(i).name;
+ header.colList.add(col);
+ }
+ rowList.add(header);
+ }
+ {
+ // 数据
+ for (int i = 0; i < dataList.size(); i++) {
+ Row row = new Row();
+ for (int j = 0; j < colMappingList.size(); j++) {
+ Row.Col col = new Row.Col();
+ Object objValue;
+
+ ColMapping tColMapping = colMappingList.get(j);
+ if (tColMapping.itemCallback == null) {
+ objValue = Ref.getFieldValue(dataList.get(i), tColMapping.field, Object.class);
+ } else {
+ objValue = tColMapping.itemCallback.handler(dataList.get(i));
+ }
+ if (tColMapping.valueCallback != null) {
+ objValue = tColMapping.valueCallback.handler(objValue.toString());
+ }
+ col.text = objValue.toString();
+ row.colList.add(col);
+ }
+ rowList.add(row);
+ }
+ }
+ // 对齐渲染
+ Map colMaxSize = new HashMap<>();
+ for (int i = 0; i < rowList.size(); i++) {
+ List colList = rowList.get(i).colList;
+ for (int j = 0; j < colList.size(); j++) {
+ String colText = colList.get(j).text;
+ if (!colMaxSize.containsKey(j)) {
+ colMaxSize.put(j, colText.length());
+ }
+ colMaxSize.put(j, Math.max(colMaxSize.get(j), colText.length()));
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < rowList.size(); i++) {
+ List colList = rowList.get(i).colList;
+ if (i == 0) {
+ // 顶边
+ sb.append(BORDER_TL);
+ for (int j = 0; j < colList.size(); j++) {
+ sb.append(BORDER_H.repeat(colMaxSize.get(j)));
+ if (j == colList.size() - 1) {
+ sb.append(BORDER_TR);
+ } else {
+ sb.append(BORDER_T);
+ }
+ }
+ sb.append('\n');
+ }
+ // 数据
+ for (int j = 0; j < colList.size(); j++) {
+ String text = colList.get(j).text;
+ if (j == 0) {
+ sb.append(BORDER_V);
+ }
+ sb.append(Text.paddedSpaceEnd(text, colMaxSize.get(j))).append(BORDER_V);
+ }
+ sb.append('\n');
+ if (i != rowList.size() - 1) {
+ // 数据分割
+ for (int j = 0; j < colList.size(); j++) {
+ if (j == 0) {
+ sb.append(BORDER_L);
+ }
+ sb.append(BORDER_H.repeat(colMaxSize.get(j)));
+ if (j == colList.size() - 1) {
+ sb.append(BORDER_R);
+ } else {
+ sb.append(BORDER_P);
+ }
+ }
+ sb.append('\n');
+ } else {
+ // 底边
+ sb.append(BORDER_BL);
+ for (int j = 0; j < colList.size(); j++) {
+ sb.append(BORDER_H.repeat(colMaxSize.get(j)));
+ if (j == colList.size() - 1) {
+ sb.append(BORDER_BR);
+ } else {
+ sb.append(BORDER_B);
+ }
+ }
+ }
+ }
+ return sb.toString();
+ } catch (IllegalAccessException e) {
+ throw new TimiException(TimiCode.ERROR, e.getMessage(), e);
+ }
+ }
+
+ public void addHeader(String field) {
+ addHeader(field.substring(0, 1).toUpperCase() + field.substring(1), field);
+ }
+
+ public void addHeader(String name, String field) {
+ TimiException.required(field, "not found field");
+
+ ColMapping colMapping = new ColMapping<>();
+ colMapping.name = name;
+ colMapping.field = field;
+ colMappingList.add(colMapping);
+ }
+
+ public void addHeader(ColMapping colMapping) {
+ colMappingList.add(colMapping);
+ }
+
+ /**
+ *
+ *
+ * @author 夜雨
+ * @since 2024-12-18 10:16
+ */
+ public static class ColMapping extends Row.Col {
+
+ private String name;
+
+ private String field;
+
+ private CallbackArgReturn itemCallback;
+
+ private CallbackArgReturn valueCallback;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getField() {
+ return field;
+ }
+
+ public void setField(String field) {
+ this.field = field;
+ }
+
+ public CallbackArgReturn getItemCallback() {
+ return itemCallback;
+ }
+
+ public void setItemCallback(CallbackArgReturn itemCallback) {
+ this.itemCallback = itemCallback;
+ }
+
+ public CallbackArgReturn getValueCallback() {
+ return valueCallback;
+ }
+
+ public void setValueCallback(CallbackArgReturn valueCallback) {
+ this.valueCallback = valueCallback;
+ }
+ }
+
+ /**
+ *
+ *
+ * @author 夜雨
+ * @since 2024-12-18 10:10
+ */
+ private static class Row {
+
+ List colList = new ArrayList<>();
+
+ /**
+ *
+ *
+ * @author 夜雨
+ * @since 2024-12-18 10:10
+ */
+ private static class Col {
+
+ String text;
+ }
+ }
+}
diff --git a/src/main/java/com/imyeyu/utils/Calc.java b/src/main/java/com/imyeyu/utils/Calc.java
new file mode 100644
index 0000000..2b0f83d
--- /dev/null
+++ b/src/main/java/com/imyeyu/utils/Calc.java
@@ -0,0 +1,204 @@
+package com.imyeyu.utils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.security.SecureRandom;
+import java.util.Random;
+
+/**
+ * 数学计算扩展
+ *
+ * @author 夜雨
+ * @version 2023-03-20 10:28
+ */
+public class Calc {
+
+ private static final SecureRandom RANDOM = new SecureRandom();
+
+ /**
+ * 是否为数字
+ *
+ * @param data 字符串
+ * @return 为 true 是表示是数字
+ */
+ public static boolean isNumber(String data) {
+ try {
+ Double.parseDouble(data);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * 向上取整返回整型
+ *
+ * @param v 数值
+ * @return 取整结果
+ */
+ public static int ceil(double v) {
+ return (int) Math.ceil(v);
+ }
+
+ /**
+ * 向下取整返回整型
+ *
+ * @param v 数值
+ * @return 取整结果
+ */
+ public static int floor(double v) {
+ return (int) Math.floor(v);
+ }
+
+ /**
+ * 四舍五入返回整型
+ *
+ * @param v 数值
+ * @return 结果
+ */
+ public static int round(double v) {
+ return (int) Math.round(v);
+ }
+
+ public static double round(double v, int scale) {
+ return round(v, scale, RoundingMode.DOWN);
+ }
+
+ public static double round(double v, int scale, RoundingMode mode) {
+ BigDecimal bd = new BigDecimal(Double.toString(v));
+ bd = bd.setScale(scale, mode);
+ return bd.doubleValue();
+ }
+
+ /**
+ * 四舍五入返回长整型
+ *
+ * @param v 数值
+ * @return 结果
+ */
+ public static long roundLong(double v) {
+ return Math.round(v);
+ }
+
+ /**
+ * 范围内取随机值 [min, max]
+ *
+ * @param min 最小值
+ * @param max 最大值
+ * @return 随机值
+ */
+ public static int random(int min, int max) {
+ if (max < min) {
+ throw new IllegalArgumentException("min must less than max");
+ }
+ return RANDOM.nextInt(max + 1 - min) + min;
+ }
+
+ /**
+ * 范围内取随机值 [min, max]
+ *
+ * @param min 最小值
+ * @param max 最大值
+ * @return 随机值
+ */
+ public static long random(long min, long max) {
+ if (max < min) {
+ throw new IllegalArgumentException("min must less than max");
+ }
+ return RANDOM.nextLong(max + 1 - min) + min;
+ }
+
+ /**
+ * 范围内取随机值 (min, max)
+ *
+ * @param min 最小值
+ * @param max 最大值
+ * @return 随机值
+ */
+ public static float random(float min, float max) {
+ if (max < min) {
+ throw new IllegalArgumentException("min must less than max");
+ }
+ return RANDOM.nextFloat(max - min) + min;
+ }
+
+ /**
+ * 范围内取随机值 (min, max)
+ *
+ * @param min 最小值
+ * @param max 最大值
+ * @return 随机值
+ */
+ public static double random(double min, double max) {
+ if (max < min) {
+ throw new IllegalArgumentException("min must less than max");
+ }
+ return RANDOM.nextDouble(max - min) + min;
+ }
+
+ public static boolean randomBoolean() {
+ return random(0, 1) == 1;
+ }
+
+ /**
+ * 计算两数差值
+ *
+ * @param v0 第一个数
+ * @param v1 第二个数
+ * @return 差值
+ * @param 类型
+ */
+ @SuppressWarnings("unchecked")
+ public static T between(T v0, T v1) {
+ if (v0 instanceof BigDecimal bd0 && v1 instanceof BigDecimal bd1) {
+ return (T) (Double) bd0.subtract(bd1).abs().doubleValue();
+ }
+ return (T) (Double) Math.abs(v0.doubleValue() - v1.doubleValue());
+ }
+
+ /**
+ * 计算一个数字是否在区间内
+ *
+ * @param min 区间最小值
+ * @param max 区间最大值
+ * @param number 判定值
+ * @return true 时,number 在 min 和 max 之间
+ */
+ public static boolean in(double min, double max, double number) {
+ if (max < min) {
+ throw new IllegalArgumentException("min must less than max");
+ }
+ return min <= number && number <= max;
+ }
+
+ /**
+ * 是否为正整数
+ *
+ * @param num 数字
+ * @return true 为正整数
+ */
+ public static boolean isN1(Long num) {
+ if (num == null) {
+ return false;
+ }
+ return 0 < num;
+ }
+
+ /**
+ * 安全限制数值范围,当入参值在最小值和最大值之间时返回原值,否则返回最小或最大值
+ *
+ * @param min 最小值
+ * @param value 入参值
+ * @param max 最大值
+ * @return 限制结果
+ */
+ public static Number range(Number min, Number value, Number max) {
+ if (value.doubleValue() < min.doubleValue()) {
+ return min;
+ }
+ if (max.doubleValue() < value.doubleValue()) {
+ return max;
+ }
+ return value;
+ }
+}
diff --git a/src/main/java/com/imyeyu/utils/Collect.java b/src/main/java/com/imyeyu/utils/Collect.java
new file mode 100644
index 0000000..6cda5be
--- /dev/null
+++ b/src/main/java/com/imyeyu/utils/Collect.java
@@ -0,0 +1,355 @@
+package com.imyeyu.utils;
+
+import com.imyeyu.java.TimiJava;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * @author 夜雨
+ * @version 2023-08-07 11:50
+ */
+public class Collect {
+
+ /**
+ * 数组元素移除
+ *
+ * @param array 源数组
+ * @param items 移除项
+ * @param 数组数据类型
+ * @return 移除结果数组
+ */
+ @SuppressWarnings("unchecked")
+ public static T[] arrayRemove(T[] array, T... items) {
+ if (array == null) {
+ return null;
+ }
+ if (TimiJava.isEmpty(array) || TimiJava.isEmpty(items)) {
+ return array;
+ }
+ Map occurrences = new HashMap<>(array.length);
+ for (T v : items) {
+ occurrences.merge(v, 1, Integer::sum);
+ }
+ BitSet toRemove = new BitSet();
+ for (int i = 0; i < array.length; i++) {
+ T key = array[i];
+ Integer count = occurrences.get(key);
+ if (count != null) {
+ occurrences.put(key, count - 1);
+ if (occurrences.get(key) == 0) {
+ occurrences.remove(key);
+ }
+ toRemove.set(i);
+ }
+ }
+ int srcLength = array.length;
+ int removals = toRemove.cardinality();
+ T[] result = (T[]) Array.newInstance(array.getClass().getComponentType(), srcLength - removals);
+ int srcIndex = 0;
+ int destIndex = 0;
+ int count;
+ int set;
+ while ((set = toRemove.nextSetBit(srcIndex)) != -1) {
+ count = set - srcIndex;
+ if (count > 0) {
+ System.arraycopy(array, srcIndex, result, destIndex, count);
+ destIndex += count;
+ }
+ srcIndex = toRemove.nextClearBit(set);
+ }
+ count = srcLength - srcIndex;
+ if (count > 0) {
+ System.arraycopy(array, srcIndex, result, destIndex, count);
+ }
+ return result;
+ }
+
+ /**
+ * 取出哈希表的键作为列表
+ *
+ * @param map 哈希表
+ * @param 键泛型
+ * @param 值泛型
+ * @return 以哈希表键为类型的列表
+ */
+ public static List mapKeys(Map map) {
+ if ((map != null) && (!map.isEmpty())) {
+ return new ArrayList<>(map.keySet());
+ }
+ return null;
+ }
+
+ /**
+ * 随机哈希表
+ *
+ * @param map 哈希表
+ * @param limit 数量限制
+ * @param 键泛型
+ * @param 值泛型
+ * @return 随机结果
+ */
+ public static Map randomMap(Map map, int limit) {
+ Map result = new LinkedHashMap<>();
+ List list = mapKeys(map);
+ list.sort((lhs, rhs) -> {
+ int r1 = (int) (Math.random() * 10 + lhs.hashCode());
+ int r2 = (int) (Math.random() * 10 + rhs.hashCode());
+ return r1 - r2;
+ });
+ for (int i = 0, l = list.size(); i < l; i++) {
+ if (result.size() < limit) {
+ result.put(list.get(i), map.get(list.get(i)));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * 根据键排序哈希表
+ *
+ * @param map 哈希表
+ * @param comparator 比较器
+ * @param 键泛型
+ * @param 值泛型
+ * @return 排序结果
+ */
+ public static Map sortMap(Map map, Comparator comparator) {
+ if (map == null) {
+ return null;
+ }
+ if (map.isEmpty()) {
+ return map;
+ }
+ Map r = new TreeMap<>(comparator);
+ r.putAll(map);
+ return r;
+ }
+
+ /**
+ * 根据数字值排序 Map(正序)
+ *
+ * @param map 哈希表
+ * @param 键类型
+ * @return 排序结果列表
+ */
+ public static LinkedHashMap sortMapByNumberValueASC(Map map) {
+ return sortMapByNumberValue(map, true);
+ }
+
+ /**
+ * 根据数字值排序 Map(倒序)
+ *
+ * @param map 哈希表
+ * @param 键类型
+ * @return 排序结果列表
+ */
+ public static LinkedHashMap sortMapByNumberValueDESC(Map map) {
+ return sortMapByNumberValue(map, false);
+ }
+
+ /**
+ * 根据数字值排序 Map
+ *
+ * @param map 哈希表
+ * @param isASC true 为正序
+ * @param 键类型
+ * @return 排序结果列表
+ */
+ public static LinkedHashMap sortMapByNumberValue(Map map, boolean isASC) {
+ return sortMapByValue(map, (o1, o2) -> {
+ if (isASC) {
+ return Double.compare(o1.getValue().doubleValue(), o2.getValue().doubleValue());
+ } else {
+ return Double.compare(o2.getValue().doubleValue(), o1.getValue().doubleValue());
+ }
+ });
+ }
+
+ /**
+ * 根据值排序 Map
+ *
+ * @param map 哈希表
+ * @param comparator 比较器
+ * @param 键类型
+ * @param 值类型
+ * @return 排序结果列表
+ */
+ public static LinkedHashMap sortMapByValue(Map map, Comparator> comparator) {
+ List> list = new ArrayList<>(map.entrySet());
+ list.sort(comparator);
+
+ LinkedHashMap result = new LinkedHashMap<>();
+ for (Map.Entry entry : list) {
+ result.put(entry.getKey(), entry.getValue());
+ }
+ return result;
+ }
+
+ /**
+ * 根据字符串键排序哈希表
+ *
+ * @param map 哈希表
+ * @param 值泛型
+ * @return 排序结果
+ */
+ public static Map sortMapByStringKeyASC(Map map) {
+ return sortMap(map, String::compareTo);
+ }
+
+ /**
+ * 根据数字键排序哈希表
+ *
+ * @param map 哈希表
+ * @param 值泛型
+ * @return 排序结果
+ */
+ public static Map sortMapByNumberKeyASC(Map map) {
+ return sortMap(map, Comparator.comparingDouble(Number::doubleValue));
+ }
+
+ /**
+ * 根据数字键排序哈希表
+ *
+ * @param map 哈希表
+ * @param 值泛型
+ * @return 排序结果
+ */
+ public static Map sortMapByNumberKeyDESC(Map map) {
+ return sortMap(map, Comparator.comparingDouble(Number::doubleValue).reversed());
+ }
+
+ /**
+ * 安全地根据键移除哈希表的对象
+ *
+ * @param map 哈希表
+ * @param key 键
+ * @param 键类型
+ * @param 值类型
+ * @return 被移除值
+ */
+ public static V removeByKey(Map map, K key) {
+ Iterator iterator = map.keySet().iterator();
+ K k;
+ while (iterator.hasNext()) {
+ k = iterator.next();
+ if (key.equals(k)) {
+ V v = map.get(k);
+ iterator.remove();
+ return v;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 安全地根据一些键移除哈希表的对象
+ *
+ * @param map 哈希表
+ * @param keys 键
+ * @param 键类型
+ * @param 值类型
+ * @return 被移除值列表
+ */
+ @SafeVarargs
+ public static List removeByKeys(Map map, K... keys) {
+ Set keysList = Set.of(keys);
+ List removes = new ArrayList<>();
+ Iterator iterator = map.keySet().iterator();
+ K k;
+ while (iterator.hasNext()) {
+ k = iterator.next();
+ if (keysList.contains(k)) {
+ removes.add(map.get(k));
+ iterator.remove();
+ }
+ }
+ return removes;
+ }
+
+ /**
+ * 安全地根据值从哈希表移除数据
+ *
+ * @param map 哈希表
+ * @param value 值
+ * @param 键类型
+ * @param 值类型
+ * @return 被移除的键
+ */
+ public static K removeByValue(Map map, V value) {
+ Iterator iterator = map.keySet().iterator();
+ K k;
+ while (iterator.hasNext()) {
+ k = iterator.next();
+ if (map.get(k).equals(value)) {
+ iterator.remove();
+ return k;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 安全地根据一些值从哈希表移除数据
+ *
+ * @param map 哈希表
+ * @param values 值
+ * @param 键类型
+ * @param 值类型
+ * @return 被移除的键列表
+ */
+ @SafeVarargs
+ public static List removeByValues(Map map, V... values) {
+ Set valuesList = Set.of(values);
+ List removes = new ArrayList<>();
+ Iterator iterator = map.keySet().iterator();
+ K k;
+ while (iterator.hasNext()) {
+ k = iterator.next();
+ if (valuesList.contains(map.get(k))) {
+ iterator.remove();
+ removes.add(k);
+ }
+ }
+ return removes;
+ }
+
+ /**
+ * 深克隆列表(通过序列化)
+ *
+ * @param list 列表
+ * @param 数据类型
+ * @return 克隆列表
+ * @throws Exception 克隆异常
+ */
+ @SuppressWarnings("unchecked")
+ public static List deepCopyList(List list) throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(baos);
+ oos.writeObject(list);
+
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ ObjectInputStream in = new ObjectInputStream(bais);
+ List result = (List) in.readObject();
+
+ in.close();
+ bais.close();
+ oos.close();
+ baos.close();
+
+ return result;
+ }
+}
diff --git a/src/main/java/com/imyeyu/utils/Decoder.java b/src/main/java/com/imyeyu/utils/Decoder.java
new file mode 100644
index 0000000..e595e40
--- /dev/null
+++ b/src/main/java/com/imyeyu/utils/Decoder.java
@@ -0,0 +1,105 @@
+package com.imyeyu.utils;
+
+import com.imyeyu.java.bean.timi.TimiCode;
+import com.imyeyu.java.bean.timi.TimiException;
+
+import java.io.StringWriter;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * 编码操作
+ *
+ * @author 夜雨
+ * @version 2021-02-13 10:59
+ */
+public class Decoder {
+
+ /**
+ * 解码 Unicode 字符串
+ *
+ * @param data Unicode 字符串
+ * @return 解码结果
+ */
+ public static String unicode(String data) {
+ StringWriter out = new StringWriter(data.length());
+
+ StringBuilder unicode = new StringBuilder(4);
+ boolean hadSlash = false;
+ boolean inUnicode = false;
+ for (int i = 0, l = data.length(); i < l; i++) {
+ char c = data.charAt(i);
+ if (inUnicode) {
+ unicode.append(c);
+ if (unicode.length() == 4) {
+ try {
+ out.write((char) Integer.parseInt(unicode.toString(), 16));
+ unicode.setLength(0);
+ inUnicode = false;
+ } catch (NumberFormatException nfe) {
+ throw new TimiException(TimiCode.ERROR, "Unable to parse unicode value: " + unicode);
+ }
+ }
+ continue;
+ }
+ if (hadSlash) {
+ hadSlash = false;
+ switch (c) {
+ case '\\' -> out.write('\\');
+ case '\'' -> out.write('\'');
+ case '\"' -> out.write('"');
+ case 'r' -> out.write('\r');
+ case 'f' -> out.write('\f');
+ case 't' -> out.write('\t');
+ case 'n' -> out.write('\n');
+ case 'b' -> out.write('\b');
+ case 'u' -> inUnicode = true;
+ default -> out.write(c);
+ }
+ continue;
+ } else if (c == '\\') {
+ hadSlash = true;
+ continue;
+ }
+ out.write(c);
+ }
+ if (hadSlash) {
+ out.write('\\');
+ }
+ return out.toString();
+ }
+
+ /**
+ * 解码 Base64 字符串
+ *
+ * @param data Base64 字符串
+ * @return 解码结果
+ */
+ public static byte[] base64(String data) {
+ return Base64.getDecoder().decode(data);
+ }
+
+ /**
+ * 解码 Base64 字符串
+ *
+ * @param data Base64 字符串
+ * @return 解码结果
+ */
+ public static String base64String(String data) {
+ return new String(base64(data), StandardCharsets.UTF_8);
+ }
+
+ /**
+ * 解码 URL 链接
+ *
+ * @param url 已编码的 URL 链接
+ * @return 解码结果
+ */
+ public static String url(String url) {
+ if (url == null) {
+ return "";
+ }
+ return URLDecoder.decode(url, StandardCharsets.UTF_8);
+ }
+}
diff --git a/src/main/java/com/imyeyu/utils/Digest.java b/src/main/java/com/imyeyu/utils/Digest.java
new file mode 100644
index 0000000..48a8cbf
--- /dev/null
+++ b/src/main/java/com/imyeyu/utils/Digest.java
@@ -0,0 +1,83 @@
+package com.imyeyu.utils;
+
+import java.math.BigInteger;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * @author 夜雨
+ * @version 2023-08-07 14:27
+ */
+public class Digest {
+
+ public static String md5(String data) throws NoSuchAlgorithmException {
+ return md5(data, StandardCharsets.UTF_8);
+ }
+
+ public static String md5(String data, Charset charset) throws NoSuchAlgorithmException {
+ return md5(data.getBytes(charset));
+ }
+
+ public static String md5(byte[] data) throws NoSuchAlgorithmException {
+ if (data == null || data.length == 0) {
+ return null;
+ }
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+ md5.update(data);
+ byte[] bytes = md5.digest();
+ char[] chars = new char[bytes.length * 2];
+ for (int i = 0, j = 0; i < bytes.length; i++) {
+ chars[j++] = Text.HEX_DIGITS_LOWER[bytes[i] >>> 4 & 0xF];
+ chars[j++] = Text.HEX_DIGITS_LOWER[bytes[i] & 0xF];
+ }
+ return new String(chars);
+ }
+
+ public static String sha1(String data) throws NoSuchAlgorithmException {
+ return sha1(data, StandardCharsets.UTF_8);
+ }
+
+ public static String sha1(String data, Charset charset) throws NoSuchAlgorithmException {
+ return sha1(data.getBytes(charset));
+ }
+
+ public static String sha1(byte[] bytes) throws NoSuchAlgorithmException {
+ MessageDigest sha = MessageDigest.getInstance("SHA");
+ sha.update(bytes);
+ return Text.byteToHex(sha.digest());
+ }
+
+ public static String sha256(String data) throws NoSuchAlgorithmException {
+ return sha256(data, StandardCharsets.UTF_8);
+ }
+
+ public static String sha256(String data, Charset charset) throws NoSuchAlgorithmException {
+ return sha256(data.getBytes(charset));
+ }
+
+ public static String sha256(byte[] bytes) throws NoSuchAlgorithmException {
+ MessageDigest sha = MessageDigest.getInstance("SHA-256");
+ sha.update(bytes);
+ return Text.byteToHex(sha.digest());
+ }
+
+ public static String sha512(String data) throws NoSuchAlgorithmException {
+ return sha512(data, StandardCharsets.UTF_8);
+ }
+
+ public static String sha512(String data, Charset charset) throws NoSuchAlgorithmException {
+ return sha512(data.getBytes(charset));
+ }
+
+ public static String sha512(byte[] bytes) throws NoSuchAlgorithmException {
+ MessageDigest sha = MessageDigest.getInstance("SHA-512");
+ BigInteger number = new BigInteger(1, sha.digest(bytes));
+ StringBuilder result = new StringBuilder(number.toString(16));
+ while (result.length() < 64) {
+ result.insert(0, "0");
+ }
+ return result.toString();
+ }
+}
diff --git a/src/main/java/com/imyeyu/utils/Encoder.java b/src/main/java/com/imyeyu/utils/Encoder.java
new file mode 100644
index 0000000..c46dd3b
--- /dev/null
+++ b/src/main/java/com/imyeyu/utils/Encoder.java
@@ -0,0 +1,162 @@
+package com.imyeyu.utils;
+
+import com.imyeyu.java.TimiJava;
+import com.imyeyu.java.bean.timi.TimiCode;
+import com.imyeyu.java.bean.timi.TimiException;
+
+import java.io.StringWriter;
+import java.net.IDN;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * 编码操作
+ *
+ * @author 夜雨
+ * @version 2021-02-13 10:59
+ */
+public class Encoder {
+
+ /**
+ * Unicode 编码全角字符
+ *
+ * @param data 字符串
+ * @return 编码结果
+ */
+ public static String unicode(String data) {
+ StringWriter out = new StringWriter(data.length() * 2);
+ for (int i = 0, l = data.length(); i < l; i++) {
+ char c = data.charAt(i);
+ String cHex = Integer.toHexString(c).toUpperCase(Locale.ENGLISH);
+
+ if (0xFFF < c) {
+ out.write("\\u" + cHex);
+ } else if (0xFF < c) {
+ out.write("\\u0" + cHex);
+ } else if (0x7F < c) {
+ out.write("\\u00" + cHex);
+ } else if (c < 32) {
+ switch (c) {
+ case '\b' -> {
+ out.write('\\');
+ out.write('b');
+ }
+ case '\n' -> {
+ out.write('\\');
+ out.write('n');
+ }
+ case '\t' -> {
+ out.write('\\');
+ out.write('t');
+ }
+ case '\f' -> {
+ out.write('\\');
+ out.write('f');
+ }
+ case '\r' -> {
+ out.write('\\');
+ out.write('r');
+ }
+ default -> {
+ if (0xF < c) {
+ out.write("\\u00" + cHex);
+ } else {
+ out.write("\\u000" + cHex);
+ }
+ }
+ }
+ } else {
+ switch (c) {
+ case '\'' -> {
+ out.write('\\');
+ out.write('\'');
+ }
+ case '"' -> {
+ out.write('\\');
+ out.write('"');
+ }
+ case '\\' -> {
+ out.write('\\');
+ out.write('\\');
+ }
+ case '/' -> {
+ out.write('\\');
+ out.write('/');
+ }
+ default -> out.write(c);
+ }
+ }
+ }
+ return out.toString();
+ }
+
+ /**
+ * Base64 编码字符串
+ *
+ * @param data 字符串
+ * @return 编码结果
+ */
+ public static String base64(String data) {
+ return base64(data.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Base64 编码字节数据
+ *
+ * @param bytes 字节数据
+ * @return 编码结果
+ */
+ public static String base64(byte[] bytes) {
+ return Base64.getEncoder().encodeToString(bytes);
+ }
+
+ /**
+ * 编码 URL 参数值。不要传整个链接,此方法只用于参数值的编码
+ *
+ * @param data URL 参数
+ * @return 编码结果
+ */
+ public static String urlArg(String data) {
+ return URLEncoder.encode(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * 编码 URL 参数
+ *
+ * @param args URL 参数表
+ * @return 编码结果
+ */
+ public static String urlArgs(Map args) {
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry param : args.entrySet()) {
+ sb.append('&').append(param.getKey()).append('=').append(urlArg(param.getValue().toString()));
+ }
+ return sb.substring(1);
+ }
+
+ /**
+ * 编码 URL 链接,半角 "+" 号会使用 "%2B"
+ *
+ * @param urlStr URL 链接
+ * @return 编码结果
+ */
+ public static String url(String urlStr) {
+ if (TimiJava.isEmpty(urlStr)) {
+ return "";
+ }
+ try {
+ URL url = new URL(urlStr);
+ URI uri = new URI(url.getProtocol(), url.getUserInfo(), IDN.toASCII(url.getHost()), url.getPort(), url.getPath(), url.getQuery(), url.getRef());
+ return uri.toASCIIString().replace("+", "%2B");
+ } catch (MalformedURLException | URISyntaxException e) {
+ throw new TimiException(TimiCode.ERROR, e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/imyeyu/utils/OS.java b/src/main/java/com/imyeyu/utils/OS.java
new file mode 100644
index 0000000..5e5f249
--- /dev/null
+++ b/src/main/java/com/imyeyu/utils/OS.java
@@ -0,0 +1,261 @@
+package com.imyeyu.utils;
+
+import com.sun.management.OperatingSystemMXBean;
+
+import java.awt.Desktop;
+import java.awt.Toolkit;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.StringSelection;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.management.ManagementFactory;
+import java.util.Comparator;
+
+/**
+ * @author 夜雨
+ * @version 2023-08-07 11:46
+ */
+public class OS {
+
+ /**
+ *
+ *
+ * @author 夜雨
+ * @since 2021-02-13 10:15
+ */
+ public interface FileSystem {
+
+ /** 文件系统路径分隔符 FileSystem.separator */
+ String SEP = File.separator;
+
+ /** 文件名排序,优先文件和文件夹,次级名称 */
+ Comparator COMPARATOR_FILE_NAME = (f1, f2) -> {
+ if (f1.isDirectory() && f2.isFile()) {
+ return -1;
+ } else {
+ if (f1.isFile() && f2.isDirectory()) {
+ return 1;
+ } else {
+ return f1.getName().compareToIgnoreCase(f2.getName());
+ }
+ }
+ };
+ }
+
+ /**
+ * 系统平台
+ *
+ * @author 夜雨
+ * @version 2024-06-22 14:41
+ */
+ public enum Platform {
+
+ WINDOWS,
+
+ LINUX,
+
+ MAC
+ }
+
+ /** 运行时系统 */
+ public static final String NAME = System.getProperty("os.name");
+
+ /** true 为 Windows 系统 */
+ public static final boolean IS_WINDOWS = NAME.toLowerCase().contains("win");
+
+ /** true 为 Mac OSX 系统 */
+ public static final boolean IS_OSX = NAME.toLowerCase().contains("mac os x");
+
+ /** true 为 UNIX 系统 */
+ public static final boolean IS_UNIX = NAME.contains("nix") || NAME.contains("nux") || NAME.contains("mac");
+
+ /** 当前系统平台 */
+ public static final Platform PLATFORM = IS_WINDOWS ? Platform.WINDOWS : (IS_OSX ? Platform.MAC : Platform.LINUX);
+
+ /**
+ * 不处理异常执行命令
+ *
+ * @param command 命令
+ */
+ public static void run(String command) {
+ try {
+ Runtime.getRuntime().exec(new String[] {command});
+ } catch (Exception ignored) {
+ }
+ }
+
+ /**
+ * 终止程序时执行命令(主线程结束后)
+ *
+ * @param command 命令
+ */
+ public static void runAfterShutdown(String command) {
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ new ProcessBuilder(command).start();
+ } catch (IOException ignored) {
+ }
+ }));
+ }
+
+ /** @return 系统内存大小(单位:字节) */
+ public static Long getSystemMemorySize() {
+ return ((OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean()).getTotalMemorySize();
+ }
+
+ /** Windows 系统禁用的字符 */
+ public static final Character[] INVALID_WINDOWS_SPECIFIC_CHARS = {'"', '*', ':', '<', '>', '?', '\\', '|', '/', 0x7F};
+
+ /** Unix 系统禁用的字符 */
+ public static final Character[] INVALID_UNIX_SPECIFIC_CHARS = {'\000'};
+
+ /**
+ * 文件名规则验证
+ *
+ * @param fileName 文件名
+ * @return true 为有效的
+ */
+ public static boolean isValidFileName(String fileName) {
+ if (fileName == null || fileName.isEmpty() || 255 < fileName.length()) {
+ return false;
+ }
+ Character[] chars;
+ if (IS_WINDOWS) {
+ chars = INVALID_WINDOWS_SPECIFIC_CHARS;
+ } else if (IS_UNIX) {
+ chars = INVALID_UNIX_SPECIFIC_CHARS;
+ } else {
+ return true;
+ }
+ for (int i = 0; i < chars.length; i++) {
+ if (fileName.contains(chars[i].toString())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 调用系统资源管理器打开位置
+ *
+ * @param dir 文件
+ */
+ public static void showInExplorer(File dir) {
+ if (dir == null || !dir.exists()) {
+ throw new IllegalArgumentException("dir is not found");
+ }
+ if (IS_WINDOWS) {
+ if (dir.isFile()) {
+ dir = dir.getParentFile();
+ }
+ run("explorer " + dir.getAbsolutePath() + FileSystem.SEP);
+ } else {
+ Desktop.getDesktop().browseFileDirectory(dir);
+ }
+ }
+
+ /**
+ * 调用系统资源管理器打开文件位置并选中
+ *
+ * @param files 文件列表
+ */
+ public static void showAndSelectInExplorer(File... files) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < files.length; i++) {
+ sb.append('"').append(files[i].getAbsolutePath()).append('"').append(',');
+ }
+ if (IS_WINDOWS) {
+ run("explorer /select," + sb.substring(0, sb.length() - 1));
+ } else {
+ Desktop.getDesktop().browseFileDirectory(files[0]);
+ }
+ }
+
+ /**
+ * 检查某程序的某进程是否在运行(Windows 方法)
+ *
+ * @param appName 程序名
+ * @param processName 进程名
+ * @param excludeProcessName 排除名称
+ * @return true 为正在运行
+ * @throws Exception 异常
+ */
+ public static boolean findProcess4Similarity(String appName, String processName, String... excludeProcessName) throws Exception {
+ return findProcess(appName, processName, true, .8F, excludeProcessName);
+ }
+
+ /**
+ * 检查某程序的某进程是否在运行(Windows 方法)
+ *
+ * @param appName 程序名
+ * @param processName 进程名
+ * @param similarity true 为启用相似度搜索
+ * @param similarityRate 相似度达到多少判定为 true
+ * @param excludeProcessName 排除名称
+ * @return true 为正在运行
+ * @throws Exception 异常
+ */
+ public static boolean findProcess(String appName, String processName, boolean similarity, float similarityRate, String... excludeProcessName) throws Exception {
+ Process proc = Runtime.getRuntime().exec("tasklist -v -fi " + '"' + "imagename eq " + appName + '"');
+ BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
+
+ int titleStart = -1; // 线程名起始字符
+ String line;
+ whi1e:
+ while ((line = br.readLine()) != null) {
+ if (titleStart == -1) {
+ if (line.startsWith("===")) {
+ titleStart = line.lastIndexOf(" ");
+ }
+ continue;
+ }
+ if (line.startsWith(appName)) {
+ // 排除名
+ if (excludeProcessName != null) {
+ for (int i = 0; i < excludeProcessName.length; i++) {
+ if (line.contains(excludeProcessName[i])) {
+ continue whi1e;
+ }
+ }
+ }
+ // 相似度匹配
+ if (similarity && titleStart < line.length()) {
+ String title = line.substring(titleStart).trim();
+ if (!title.equals("")) {
+ if (similarityRate < Text.similarityRatio(processName, title)) {
+ return true;
+ }
+ }
+ } else {
+ return line.contains(processName);
+ }
+ }
+ }
+ br.close();
+ return false;
+ }
+
+ /**
+ * 设置字符串到剪切板(复制)
+ *
+ * @param s 字符串
+ */
+ public static void setIntoClipboard(String s) {
+ Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(s), null);
+ }
+
+ /**
+ * 获取剪切版的字符串(粘贴)
+ *
+ * @return 剪切板字符串,如果剪切板没有字符串将返回空的字符串
+ */
+ public static String getIntoClipboard() {
+ try {
+ return Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null).getTransferData(DataFlavor.stringFlavor).toString();
+ } catch (Exception e) {
+ return "";
+ }
+ }
+}
diff --git a/src/main/java/com/imyeyu/utils/StringInterpolator.java b/src/main/java/com/imyeyu/utils/StringInterpolator.java
new file mode 100644
index 0000000..fef9566
--- /dev/null
+++ b/src/main/java/com/imyeyu/utils/StringInterpolator.java
@@ -0,0 +1,128 @@
+package com.imyeyu.utils;
+
+import com.imyeyu.java.TimiJava;
+import com.imyeyu.java.bean.CallbackArgReturn;
+import com.imyeyu.java.ref.Ref;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * 字符串插值器
+ *
+ * @author 夜雨
+ * @version 2022-12-01 00:41
+ */
+public class StringInterpolator {
+
+ /** ${} 插值正则 */
+ public static final String DOLLAR_OBJ = "\\$\\{(.+?)\\}";
+
+ /** {} 插值正则 */
+ public static final String SIMPLE_OBJ = "\\{(.+?)\\}";
+
+ /** 正则匹配器 */
+ private final Pattern pattern;
+
+ /** 为 true 时允许安全插值空,变量可以插值 null 而不抛出异常 */
+ private boolean nullable = false;
+
+ /** 过滤器 */
+ private Map> filterMap;
+
+ /**
+ * 默认构造
+ *
+ * @param regex 插槽正则
+ */
+ public StringInterpolator(String regex) {
+ this.pattern = Pattern.compile(regex);
+ }
+
+ /**
+ * 注入变量
+ *
+ * @param string 字符串
+ * @param argsMap 变量表
+ * @return 插值结果
+ */
+ public String inject(String string, Map argsMap) {
+ return pattern.matcher(string).replaceAll(result -> {
+ String group = result.group(1);
+ String key = group.trim();
+ String[] filters = null;
+ if (group.contains("|")) {
+ // 过滤器
+ String[] groups = group.split("\\|");
+ key = groups[0].trim();
+ filters = new String[groups.length - 1];
+ System.arraycopy(groups, 1, filters, 0, filters.length);
+ }
+ Object value = argsMap.get(key);
+ if (key.contains(".")) {
+ // 反射获取
+ try {
+ String[] deepKey = key.split("\\.");
+ value = argsMap.get(deepKey[0]);
+ for (int i = 1; i < deepKey.length; i++) {
+ value = Ref.getFieldValue(value, deepKey[i], Object.class);
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("ref field file: " + e.getMessage(), e);
+ }
+ }
+ if (TimiJava.isNotEmpty(filterMap) && TimiJava.isNotEmpty(filters)) {
+ // 过滤
+ for (int i = 0; i < filters.length; i++) {
+ CallbackArgReturn filter = filterMap.get(filters[i].trim());
+ if (filter == null) {
+ throw new NullPointerException("not found %s filter for %s".formatted(filters[i], key));
+ }
+ value = filter.handler((String) value);
+ }
+ }
+ if (value == null) {
+ if (!nullable) {
+ throw new NullPointerException("null pointer exception for arg: " + key);
+ }
+ value = "";
+ }
+ return String.valueOf(value);
+ });
+ }
+
+ public void putFilter(String name, CallbackArgReturn callback) {
+ if (filterMap == null) {
+ filterMap = new HashMap<>();
+ }
+ filterMap.put(name, callback);
+ }
+
+ public void putAllFilter(Map> filterMap) {
+ if (this.filterMap == null) {
+ this.filterMap = new HashMap<>();
+ }
+ this.filterMap.putAll(filterMap);
+ }
+
+ public void removeFilter(String name) {
+ if (TimiJava.isNotEmpty(filterMap)) {
+ filterMap.remove(name);
+ }
+ }
+
+ public void clearFilter() {
+ if (TimiJava.isNotEmpty(filterMap)) {
+ filterMap.clear();
+ }
+ }
+
+ public void setNullable(boolean nullable) {
+ this.nullable = nullable;
+ }
+
+ public boolean isNullable() {
+ return nullable;
+ }
+}
diff --git a/src/main/java/com/imyeyu/utils/Text.java b/src/main/java/com/imyeyu/utils/Text.java
new file mode 100644
index 0000000..9ef93cb
--- /dev/null
+++ b/src/main/java/com/imyeyu/utils/Text.java
@@ -0,0 +1,463 @@
+package com.imyeyu.utils;
+
+import com.imyeyu.java.TimiJava;
+
+import java.io.UnsupportedEncodingException;
+import java.security.SecureRandom;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.regex.Pattern;
+
+/**
+ * @author 夜雨
+ * @version 2023-08-07 11:58
+ */
+public class Text {
+
+ /** 十六进制小写 */
+ public static char[] HEX_DIGITS_LOWER = "0123456789abcdef".toCharArray();
+
+ /** 十六进制大写 */
+ public static char[] HEX_DIGITS_UPPER = "0123456789ABCDEF".toCharArray();
+
+ /**
+ * 字节数据转 16 进制字符串
+ *
+ * @param bytes 字节数据
+ * @return 16 进制字符串
+ */
+ public static String byteToHex(byte[] bytes) {
+ final int l = bytes.length;
+ final char[] c = new char[l << 1];
+ for (int i = 0, j = 0; i < l; i++) {
+ c[j++] = Text.HEX_DIGITS_LOWER[(0xF0 & bytes[i]) >>> 4];
+ c[j++] = Text.HEX_DIGITS_LOWER[0x0F & bytes[i]];
+ }
+ return new String(c);
+ }
+
+ /**
+ * 16 进制字符串转字节数据
+ *
+ * @param hex 16 进制字符串
+ * @return 字节数据
+ * @throws UnsupportedEncodingException 不支持的编码
+ */
+ public static byte[] hexToByte(String hex) throws UnsupportedEncodingException {
+ final char[] c = hex.toCharArray();
+ final byte[] b = new byte[c.length >> 1];
+
+ final int len = c.length;
+ if ((len & 0x01) != 0) {
+ throw new UnsupportedEncodingException("Odd number of characters.");
+ }
+
+ final int outLen = len >> 1;
+ if (c.length < outLen) {
+ throw new UnsupportedEncodingException("Output array is not large enough to accommodate decoded data.");
+ }
+ for (int i = 0, j = 0; j < len; i++) {
+ int f = toDigit(c[j], j) << 4;
+ j++;
+ f = f | toDigit(c[j], j);
+ j++;
+ b[i] = (byte) (f & 0xFF);
+ }
+ return b;
+ }
+
+ private static int toDigit(final char ch, final int index) throws UnsupportedEncodingException {
+ final int digit = Character.digit(ch, 16);
+ if (digit == -1) {
+ throw new UnsupportedEncodingException("Illegal hexadecimal character " + ch + " at index " + index);
+ }
+ return digit;
+ }
+
+ /**
+ * 是否为半角字符
+ *
+ * @param c 字符
+ * @return 为 true 是表示是半角字符
+ */
+ public static boolean isHalfChar(char c) {
+ return (int) c < 129;
+ }
+
+ /**
+ * 字符串加双引号
+ *
+ * @param text 字符串内容
+ * @return 结果
+ */
+ public static String quote(String text) {
+ return '"' + text + '"';
+ }
+
+ /**
+ * 前补零(最终长度 2 字符)
+ *
+ * @param number 数值
+ * @return 补零字符串
+ */
+ public static String zero(Number number) {
+ return zero(2, number);
+ }
+
+ public static String paddedSpaceStart(String str, int totalWidth) {
+ return String.format("%" + totalWidth + "s", str);
+ }
+
+ public static String paddedSpaceEnd(String str, int totalWidth) {
+ return String.format("%-" + totalWidth + "s", str);
+ }
+
+ /**
+ * 前补零
+ *
+ * @param l 最终长度
+ * @param number 数值
+ * @return 补零字符串
+ */
+ public static String zero(int l, Number number) {
+ return String.format("%0" + l + "d", number);
+ }
+
+ /**
+ * 正则表达式测试
+ *
+ * @param reg 正则
+ * @param value 文本
+ * @return true 为匹配
+ */
+ public static boolean testReg(String reg, String value) {
+ return Pattern.compile(reg).matcher(value).matches();
+ }
+
+ /**
+ * 驼峰转下划线
+ *
+ * @param camelCaseStr 驼峰字符串
+ * @return 下划线字符串
+ */
+ public static String camelCase2underscore(String camelCaseStr) {
+ return camelCaseStr.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
+ }
+
+ /**
+ * 下划线转驼峰
+ *
+ * @param underscoreName 下划线字符串
+ * @return 驼峰字符串
+ */
+ public static String underscore2camelCase(String underscoreName) {
+ if (TimiJava.isEmpty(underscoreName)) {
+ return underscoreName;
+ }
+ StringBuilder result = new StringBuilder();
+ boolean flag = false;
+ for (int i = 0; i < underscoreName.length(); i++) {
+ char c = underscoreName.charAt(i);
+ if ('_' == c) {
+ flag = true;
+ } else {
+ if (flag) {
+ result.append(Character.toUpperCase(c));
+ flag = false;
+ } else {
+ result.append(c);
+ }
+ }
+ }
+ return result.toString();
+ }
+
+
+ /**
+ * 与多个字符串进行与比较
+ *
+ * @param string 比较字符串
+ * @param other 其他字符串
+ * @return true 时全部其他字符串和比较字符串一致
+ */
+ public static boolean eqAnd(String string, String... other) {
+ for (int i = 0; i < other.length; i++) {
+ if (!string.equals(other[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 与多个字符串进行或比较
+ *
+ * @param string 比较字符串
+ * @param other 其他字符串
+ * @return true 时其他字符串存在和比较字符串一致
+ */
+ public static boolean eqOr(String string, String... other) {
+ for (int i = 0; i < other.length; i++) {
+ if (!string.equals(other[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 与多个字符串进行忽略大小写的与比较
+ *
+ * @param string 比较字符串
+ * @param other 其他字符串
+ * @return true 时全部其他字符串和比较字符串一致
+ */
+ public static boolean eqIgnoreCaseAnd(String string, String... other) {
+ for (int i = 0; i < other.length; i++) {
+ if (!string.equalsIgnoreCase(other[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 与多个字符串进行忽略大小写的或比较
+ *
+ * @param string 比较字符串
+ * @param other 其他字符串
+ * @return true 时其他字符串存在和比较字符串一致
+ */
+ public static boolean eqIgnoreCaseOr(String string, String... other) {
+ for (int i = 0; i < other.length; i++) {
+ if (string.equalsIgnoreCase(other[i])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 与多个字符串进行忽略大小写包含关系
+ *
+ * @param string 原字符串
+ * @param other 其他字符串
+ * @return true 为 string 中至少含有一个 other 的忽略大小写的字符段
+ */
+ public static boolean containsIgnoreCase(String string, String... other) {
+ String stringUpper = string.toUpperCase();
+ String stringLower = string.toLowerCase();
+ for (int i = 0; i < other.length; i++) {
+ if (stringLower.contains(other[i].toLowerCase()) || stringUpper.contains(other[i].toUpperCase())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static String randomString(int length) {
+ return randomString("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", length);
+ }
+
+ public static String randomString(String pool, int length) {
+ SecureRandom r = new SecureRandom();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ sb.append(pool.charAt(r.nextInt(pool.length() - 1)));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 较短的临时 UUID,如果使用在庞大的数据里,很可能会发生重复
+ *
+ * @return 完整 UUID 前 8 位
+ */
+ public static String tempUUID() {
+ return UUID.randomUUID().toString().substring(0, 8);
+ }
+
+ /**
+ * 计算字符串相似度(编辑距离算法)
+ *
+ * @param source 需比较的字符串
+ * @param target 被比较的字符串
+ * @param isIgnore 为 true 时忽略大小写
+ * @return 相似度 [0, 1]
+ */
+ private static float levenshteinDistance(String source, String target, boolean isIgnore) {
+ int[][] d;
+ int n = source.length(), m = target.length(), i, j, temp;
+ char charS, charT;
+
+ if (n == 0) {
+ return m;
+ }
+ if (m == 0) {
+ return n;
+ }
+
+ d = new int[n + 1][m + 1];
+ for (i = 0; i <= n; i++) {
+ d[i][0] = i;
+ }
+ for (j = 0; j <= m; j++) {
+ d[0][j] = j;
+ }
+ for (i = 1; i <= n; i++) {
+ charS = source.charAt(i - 1);
+ for (j = 1; j <= m; j++) {
+ charT = target.charAt(j - 1);
+ if (isIgnore) {
+ if (charS == charT || charS == charT + 32 || charS + 32 == charT) {
+ temp = 0;
+ } else {
+ temp = 1;
+ }
+ } else {
+ if (charS == charT) {
+ temp = 0;
+ } else {
+ temp = 1;
+ }
+ }
+ d[i][j] = min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + temp);
+ }
+ }
+ return d[n][m];
+ }
+
+ /**
+ * 三数求最小
+ *
+ * @param one 值一
+ * @param two 值二
+ * @param three 值三
+ * @return 最小值
+ */
+ public static int min(int one, int two, int three) {
+ return (one = Math.min(one, two)) < three ? one : three;
+ }
+
+ /**
+ * 批量相似度比较字符串(忽略大小写),返回相似度比较列表倒叙结果,较为相似的排最前
+ *
+ * @param sources 需比较的字符串列表
+ * @param target 被比较的字符串
+ * @return 比较结果列表
+ */
+ public static LinkedHashMap similarityRatioList(Collection sources, String target) {
+ return similarityRatioList(sources, target, true);
+ }
+
+ /**
+ * 批量相似度比较字符串,返回相似度比较列表倒叙结果,较为相似的排最前
+ *
+ * @param sources 需比较的字符串列表
+ * @param target 被比较的字符串
+ * @param isIgnoreCase true 为忽略大小写
+ * @return 比较结果列表
+ */
+ public static LinkedHashMap similarityRatioList(Collection sources, String target, boolean isIgnoreCase) {
+ Map items = new HashMap<>();
+ for (String source : sources) {
+ items.put(source, 0);
+ }
+ items.replaceAll((k, v) -> similarityRatio(k, target, isIgnoreCase));
+ return Collect.sortMapByNumberValueDESC(items);
+ }
+
+ /**
+ * 求字符串相似度,忽略大小写
+ *
+ * @param source 需比较的字符串
+ * @param target 被比较的字符串
+ * @return 相似度 [0, 1]
+ */
+ public static float similarityRatio(String source, String target) {
+ return similarityRatio(source, target, true);
+ }
+
+ /**
+ * 求字符串相似度
+ *
+ * @param source 需比较的字符串
+ * @param target 被比较的字符串
+ * @param isIgnoreCase true 为忽略大小写
+ * @return 相似度 [0, 1]
+ */
+ public static float similarityRatio(String source, String target, boolean isIgnoreCase) {
+ float ret;
+ final int max = Math.max(source.length(), target.length());
+ if (max == 0) {
+ ret = 1;
+ } else {
+ ret = 1 - levenshteinDistance(source, target, isIgnoreCase) / max;
+ }
+ return ret;
+ }
+
+ /**
+ * 检验字符串是否为 json 数据,不校验是否有错误
+ *
+ * @param s 字符串
+ * @return true 为是 JSON 数据
+ */
+ public static boolean isJson(String s) {
+ return isJsonObject(s) || isJsonArray(s);
+ }
+
+ /**
+ * 检验字符串是否为 json 对象,不校验是否有错误
+ *
+ * @param s 字符串
+ * @return true 为是 JSON 对象
+ */
+ public static boolean isJsonObject(String s) {
+ return s.startsWith("{") && s.endsWith("}");
+ }
+
+
+ /**
+ * 检验字符串是否为 json 数组,不校验是否有错误
+ *
+ * @param s 字符串
+ * @return true 为是 JSON 数组
+ */
+ public static boolean isJsonArray(String s) {
+ return s.startsWith("[") && s.endsWith("]");
+ }
+
+
+ /**
+ * 字符串替换,不需要正则的情况下
+ *
+ * @param string 字符串
+ * @param from 被替换字符
+ * @param to 替换字符串
+ * @return 替换结果
+ */
+ public static String replaceAll(String string, char from, String to) {
+ return replaceAll(new StringBuilder(string), from, to);
+ }
+
+ /**
+ * 字符串替换,不需要正则的情况下
+ *
+ * @param sb 字符构造器
+ * @param from 被替换字符
+ * @param to 替换字符串
+ * @return 替换结果
+ */
+ public static String replaceAll(StringBuilder sb, char from, String to) {
+ for (int i = 0; i < sb.length(); i++) {
+ if (sb.charAt(i) == from) {
+ sb.replace(i, ++i, to);
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/imyeyu/utils/Time.java b/src/main/java/com/imyeyu/utils/Time.java
new file mode 100644
index 0000000..1c844c2
--- /dev/null
+++ b/src/main/java/com/imyeyu/utils/Time.java
@@ -0,0 +1,417 @@
+package com.imyeyu.utils;
+
+import com.imyeyu.java.TimiJava;
+
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 时间转换相关
+ *
+ * @author 夜雨
+ * @version 2021-06-10 20:07
+ */
+public class Time {
+
+ // ---------- 格式化 ----------
+
+ /** 格式化 yy */
+ public static final SimpleDateFormat year = new SimpleDateFormat("yy");
+
+ /** 格式化 yyyy */
+ public static final SimpleDateFormat yearFull = new SimpleDateFormat("yyyy");
+
+ /** 格式化 M */
+ public static final SimpleDateFormat month = new SimpleDateFormat("M");
+
+ /** 格式化 MM */
+ public static final SimpleDateFormat monthFull = new SimpleDateFormat("MM");
+
+ /** 格式化 d */
+ public static final SimpleDateFormat day = new SimpleDateFormat("d");
+
+ /** 格式化 dd */
+ public static final SimpleDateFormat dayFull = new SimpleDateFormat("dd");
+
+ /** 格式化 HH */
+ public static final SimpleDateFormat hour = new SimpleDateFormat("HH");
+
+ /** 格式化 mm */
+ public static final SimpleDateFormat minute = new SimpleDateFormat("mm");
+
+ /** 格式化 ss */
+ public static final SimpleDateFormat second = new SimpleDateFormat("ss");
+
+ /** 格式化 hh:mm */
+ public static final SimpleDateFormat hhmm = new SimpleDateFormat("HH:mm");
+
+ /** 格式化 mm:ss */
+ public static final SimpleDateFormat mmss = new SimpleDateFormat("mm:ss");
+
+ /** 格式化 yyyyMMdd */
+ public static final SimpleDateFormat ymd = new SimpleDateFormat("yyyyMMdd");
+
+ /** 格式化 HH:mm:ss.SSS */
+ public static final SimpleDateFormat log = new SimpleDateFormat("[HH:mm:ss.SSS]");
+
+ /** 格式化 HH:mm:ss.SSS */
+ public static final SimpleDateFormat longLog = new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss.SSS]");
+
+ /** 格式化 yyyy-MM-dd */
+ public static final SimpleDateFormat date = new SimpleDateFormat("yyyy-MM-dd");
+
+ /** 格式化 HH:mm:ss */
+ public static final SimpleDateFormat time = new SimpleDateFormat("HH:mm:ss");
+
+ /** 格式化 yyyy-MM-dd HH:mm:ss */
+ public static final SimpleDateFormat dateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+ /** 格式化 yyyy-MM-dd'T'HH:mm:ss */
+ public static final SimpleDateFormat dateTimeT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+
+ // ---------- 长整型时间戳 ----------
+
+ /** 1 秒时间戳 */
+ public static final long S = 1000;
+
+ /** 1 分钟时间戳 */
+ public static final long M = S * 60;
+
+ /** 1 小时时间戳 */
+ public static final long H = M * 60;
+
+ /** 1 天时间戳 */
+ public static final long D = H * 24;
+
+ // ---------- 整型时间戳 ----------
+
+ /** 1 秒时间戳(整型) */
+ public static final int SI = 1000;
+
+ /** 1 分钟时间戳(整型) */
+ public static final int MI = SI * 60;
+
+ /** 1 小时时间戳(整型) */
+ public static final int HI = MI * 60;
+
+ /** 1 天时间戳(整型) */
+ public static final int DI = HI * 24;
+
+ /**
+ * 计算两个时间戳精确的日期时间差
+ *
+ * @param begin 开始时间戳
+ * @param end 结束时间戳
+ * @return 时差
+ */
+ public static Between between(long begin, long end) {
+ if (end < begin) {
+ throw new IllegalArgumentException("end time must greater than begin time:" + end + " < " + begin);
+ }
+ LocalDateTime ldtBegin = toLocalDateTime(begin);
+ LocalDateTime ldtEnd = toLocalDateTime(end);
+
+ Between between = new Between();
+ between.year = (int) ChronoUnit.YEARS.between(ldtBegin, ldtEnd);
+ between.month = (int) ChronoUnit.MONTHS.between(ldtBegin, ldtEnd) % 12;
+ between.day = (int) ChronoUnit.DAYS.between(ldtBegin.plusMonths(between.year * 12L + between.month), ldtEnd);
+ between.hour = (int) (ChronoUnit.HOURS.between(ldtBegin, ldtEnd) - ChronoUnit.DAYS.between(ldtBegin, ldtEnd) * 24);
+ between.minute = (int) ChronoUnit.MINUTES.between(ldtBegin, ldtEnd) % 60;
+ between.second = (int) ChronoUnit.SECONDS.between(ldtBegin, ldtEnd) % 60;
+ between.millis = (int) ((end - begin) % 1000);
+ return between;
+ }
+
+ /**
+ * 获取此刻毫秒
+ *
+ * @return 毫秒
+ */
+ public static long now() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ * 获取当前时间 yyyy-MM-dd HH:mm:ss
+ *
+ * @return 当前时间
+ */
+ public static String nowString() {
+ return toDateTime(new Date());
+ }
+
+ /**
+ * 获取昨天零时时间戳
+ *
+ * @return 昨天零时时间戳
+ */
+ public static long yesterday() {
+ return today() - D;
+ }
+
+ /**
+ * 获取今天零时时间戳
+ *
+ * @return 今天零时时间戳
+ */
+ public static long today() {
+ long now = now();
+ final long H8 = H * 8;
+ return ((now + H8 - (now + H8) % (H * 24)) - H8);
+ }
+
+ /**
+ * 获取明天零时时间戳
+ *
+ * @return 明天零时时间戳
+ */
+ public static long tomorrow() {
+ return today() + D;
+ }
+
+ /**
+ * 转义为日期 yyyy-MM-dd
+ *
+ * @param unixTime 时间戳
+ * @return 日期字符串
+ */
+ public static String toDate(long unixTime) {
+ return date.format(unixTime);
+ }
+
+ /**
+ * 转义为时间 HH:mm:ss
+ *
+ * @param unixTime 时间戳
+ * @return 时间字符串
+ */
+ public static String toTime(long unixTime) {
+ return time.format(unixTime);
+ }
+
+ /**
+ * 转义为日期时间 yyyy-MM-dd HH:mm:ss
+ *
+ * @param unixTime 时间戳
+ * @return 日期时间字符串
+ */
+ public static String toDateTime(long unixTime) {
+ return dateTime.format(unixTime);
+ }
+
+ /**
+ * 转义为日期时间 yyyy-MM-dd HH:mm:ss
+ *
+ * @param date 时间对象
+ * @return 日期时间字符串
+ */
+ public static String toDateTime(Date date) {
+ return dateTime.format(date);
+ }
+
+ /**
+ * 本地时间对象转时间戳(本地时区)
+ *
+ * @param date 本地日期对象
+ * @return 时间戳
+ */
+ public static Long fromLocalDate(LocalDate date) {
+ if (date == null) {
+ return null;
+ }
+ return date.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+ }
+
+ /**
+ * 本地时间对象转时间戳(本地时区)
+ *
+ * @param dateTime 本地日期对象
+ * @return 时间戳
+ */
+ public static Long fromLocalDateTime(LocalDateTime dateTime) {
+ if (dateTime == null) {
+ return null;
+ }
+ return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+ }
+
+ /**
+ * 时间戳转本地日期(本地时区)
+ *
+ * @param unixTime 时间戳
+ * @return 本地日期对象
+ */
+ public static LocalDateTime toLocalDateTime(Long unixTime) {
+ if (unixTime == null) {
+ return null;
+ }
+ return Instant.ofEpochMilli(unixTime).atZone(ZoneId.systemDefault()).toLocalDateTime();
+ }
+
+ /**
+ * 媒体时间操作
+ *
+ * @author 夜雨
+ * @version 2023-01-24 01:10
+ */
+ public static class Media {
+
+ private static final Pattern PATTERN = Pattern.compile("(\\d*):?([0-5]\\d):([0-5]\\d)\\.?(\\d{1,3})?");
+
+ /**
+ * 解析字符串为毫秒,匹配 999:59:59.999 格式,兼容 999:59:59
+ *
+ * @param time 时间字符串
+ * @return 毫秒
+ */
+
+ public static long fromString(String time) {
+ if (TimiJava.isEmpty(time)) {
+ return 0;
+ }
+ Matcher matcher = PATTERN.matcher(time);
+ if (matcher.find()) {
+ long h = 0;
+ if (TimiJava.isNotEmpty(matcher.group(1))) {
+ h = Long.parseLong(matcher.group(1));
+ }
+ long m = Long.parseLong(matcher.group(2));
+ long s = Long.parseLong(matcher.group(3));
+ long ms = 0;
+ if (TimiJava.isNotEmpty(matcher.group(4))) {
+ String gms = matcher.group(4);
+ ms = Long.parseLong(gms);
+ if (gms.length() == 2) {
+ ms *= 10;
+ }
+ }
+ return h * Time.H + m * Time.M + s * Time.S + ms;
+ }
+ return 0;
+ }
+
+ /**
+ * 秒转媒体时间,00:00:00
+ *
+ * @param second 秒
+ * @return 时间字符串
+ */
+ public static String toString(double second) {
+ return toString((int) second);
+ }
+
+ /**
+ * 秒转媒体时间,00:00:00
+ *
+ * @param second 秒
+ * @return 时间字符串
+ */
+ public static String toString(int second) {
+ int h = second / 60 / 60;
+ if (0 < h) {
+ return String.format("%d:%02d:%02d", h, second / 60 - h * 60, second % 60);
+ } else {
+ return String.format("%02d:%02d", second / 60, second % 60);
+ }
+ }
+
+ /**
+ * 毫秒转媒体时间,音视频播放时间,00:00:00.000
+ *
+ * @param millis 毫秒
+ * @return 时间字符串
+ */
+ public static String toString(long millis) {
+ long s = millis / 1000;
+ long h = s / 60 / 60;
+ return String.format("%d:%02d:%02d.%03d", h, s / 60 - h * 60, s % 60, millis % 1000);
+ }
+ }
+
+ /**
+ * 时差
+ *
+ * @author 夜雨
+ * @version 2022-10-12 14:46
+ */
+ public static class Between {
+
+ int year;
+ int month;
+ int day;
+ int hour;
+ int minute;
+ int second;
+ int millis;
+
+ /**
+ * 获取年数
+ *
+ * @return 年数
+ */
+ public int getYear() {
+ return year;
+ }
+
+ /**
+ * 获取月数
+ *
+ * @return 月数
+ */
+ public int getMonth() {
+ return month;
+ }
+
+ /**
+ * 获取天数
+ *
+ * @return 天数
+ */
+ public int getDay() {
+ return day;
+ }
+
+ /**
+ * 获取小时
+ *
+ * @return 小时
+ */
+ public int getHour() {
+ return hour;
+ }
+
+ /**
+ * 获取分钟
+ *
+ * @return 分钟
+ */
+ public int getMinute() {
+ return minute;
+ }
+
+ /**
+ * 获取秒
+ *
+ * @return 秒
+ */
+ public int getSecond() {
+ return second;
+ }
+
+ /**
+ * 获取毫秒
+ *
+ * @return 毫秒
+ */
+ public int getMillis() {
+ return millis;
+ }
+ }
+}
diff --git a/src/test/java/test/TestAsciiTable.java b/src/test/java/test/TestAsciiTable.java
new file mode 100644
index 0000000..8303773
--- /dev/null
+++ b/src/test/java/test/TestAsciiTable.java
@@ -0,0 +1,40 @@
+package test;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import com.imyeyu.utils.AsciiTable;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author 夜雨
+ * @since 2024-12-18 12:47
+ */
+public class TestAsciiTable {
+
+ @Test
+ public void testTable() throws Exception {
+ List- dataList = new ArrayList<>();
+ dataList.add(new Item("r1 f1 value", "r1 f2 value"));
+ dataList.add(new Item("r2 f1 value", "r2 f2 value"));
+ dataList.add(new Item("r3 f1 value", "r3 f2 value333333"));
+ dataList.add(new Item("r4 f1 value", "r4 f2 value"));
+
+ AsciiTable
- table = new AsciiTable<>();
+ table.addHeader("f1 bbbbbbbbbbbbbbbbbbbbbbbbbbbb", "f1");
+ table.addHeader("f2");
+ String result = table.render(dataList);
+ System.out.println(result);
+ }
+
+ @Data
+ @AllArgsConstructor
+ public static class Item {
+
+ String f1;
+
+ String f2;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/test/TestText.java b/src/test/java/test/TestText.java
new file mode 100644
index 0000000..2d78c44
--- /dev/null
+++ b/src/test/java/test/TestText.java
@@ -0,0 +1,47 @@
+package test;
+
+import com.imyeyu.utils.StringInterpolator;
+import com.imyeyu.utils.Text;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-04 17:50
+ */
+public class TestText {
+
+ @Test
+ public void testStringInterpolator() {
+ String template = "test ${item.name} deep arg";
+
+ StringInterpolator interpolator = new StringInterpolator(StringInterpolator.DOLLAR_OBJ);
+
+ Map argsMap = new HashMap<>();
+ Item item = new Item();
+ item.name = "deepName";
+ argsMap.put("item", item);
+
+ System.out.println(interpolator.inject(template, argsMap));
+ }
+
+ @Test
+ public void testCamelToUnderline() {
+ String str = "";
+ String result = "";
+ assert Text.camelCase2underscore(str).equals(result);
+ str = "hello";
+ result = "hello";
+ assert Text.camelCase2underscore(str).equals(result);
+ str = "helloWorldTest";
+ result = "hello_world_test";
+ assert Text.camelCase2underscore(str).equals(result);
+ }
+
+ public static class Item {
+
+ String name;
+ }
+}
diff --git a/src/test/java/test/TestTime.java b/src/test/java/test/TestTime.java
new file mode 100644
index 0000000..1bf51fe
--- /dev/null
+++ b/src/test/java/test/TestTime.java
@@ -0,0 +1,8 @@
+package test;
+
+/**
+ * @author 夜雨
+ * @since 2024-12-19 22:10
+ */
+public class TestTime {
+}
\ No newline at end of file