From abf09cee047c57e3a77a6cba3770b634d2446de1 Mon Sep 17 00:00:00 2001 From: Timi Date: Sat, 12 Jul 2025 12:45:46 +0800 Subject: [PATCH] Initial project --- .gitignore | 128 ++--- .idea/.gitignore | 3 + .idea/CopilotChatHistory.xml | 27 + .idea/encodings.xml | 7 + .idea/misc.xml | 14 + .idea/uiDesigner.xml | 124 +++++ pom.xml | 37 ++ .../java/com/imyeyu/utils/AsciiTable.java | 250 ++++++++++ src/main/java/com/imyeyu/utils/Calc.java | 204 ++++++++ src/main/java/com/imyeyu/utils/Collect.java | 355 ++++++++++++++ src/main/java/com/imyeyu/utils/Decoder.java | 105 ++++ src/main/java/com/imyeyu/utils/Digest.java | 83 ++++ src/main/java/com/imyeyu/utils/Encoder.java | 162 ++++++ src/main/java/com/imyeyu/utils/OS.java | 261 ++++++++++ .../com/imyeyu/utils/StringInterpolator.java | 128 +++++ src/main/java/com/imyeyu/utils/Text.java | 463 ++++++++++++++++++ src/main/java/com/imyeyu/utils/Time.java | 417 ++++++++++++++++ src/test/java/test/TestAsciiTable.java | 40 ++ src/test/java/test/TestText.java | 47 ++ src/test/java/test/TestTime.java | 8 + 20 files changed, 2769 insertions(+), 94 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/CopilotChatHistory.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/uiDesigner.xml create mode 100644 pom.xml create mode 100644 src/main/java/com/imyeyu/utils/AsciiTable.java create mode 100644 src/main/java/com/imyeyu/utils/Calc.java create mode 100644 src/main/java/com/imyeyu/utils/Collect.java create mode 100644 src/main/java/com/imyeyu/utils/Decoder.java create mode 100644 src/main/java/com/imyeyu/utils/Digest.java create mode 100644 src/main/java/com/imyeyu/utils/Encoder.java create mode 100644 src/main/java/com/imyeyu/utils/OS.java create mode 100644 src/main/java/com/imyeyu/utils/StringInterpolator.java create mode 100644 src/main/java/com/imyeyu/utils/Text.java create mode 100644 src/main/java/com/imyeyu/utils/Time.java create mode 100644 src/test/java/test/TestAsciiTable.java create mode 100644 src/test/java/test/TestText.java create mode 100644 src/test/java/test/TestTime.java 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