[공통] util → framework 유틸 #7
8 changed files with 700 additions and 287 deletions
3
.forge/ACM-CM-002-attempt-3-run-d48451005e39.md
Normal file
3
.forge/ACM-CM-002-attempt-3-run-d48451005e39.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# ACM-CM-002-attempt-3-run-d48451005e39
|
||||
|
||||
Forge 이슈 작업 브랜치 `forge/ACM-CM-002-attempt-3-run-d48451005e39`.
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.klaro.acquirecore.framework.util;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
/**
|
||||
* Static utility class for amount/money operations migrated from legacy C util_amount.c
|
||||
*/
|
||||
public final class AmountUtil {
|
||||
|
||||
private static final int SCALE = 2;
|
||||
private static final RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP;
|
||||
|
||||
private AmountUtil() {
|
||||
// Utility class - prevent instantiation
|
||||
}
|
||||
|
||||
public static BigDecimal fromCents(long cents) {
|
||||
return BigDecimal.valueOf(cents, SCALE);
|
||||
}
|
||||
|
||||
public static BigDecimal fromString(String str) {
|
||||
if (str == null || str.isEmpty()) return null;
|
||||
return new BigDecimal(str).setScale(SCALE, ROUNDING_MODE);
|
||||
}
|
||||
|
||||
public static String toString(BigDecimal amount) {
|
||||
return amount == null ? null : amount.setScale(SCALE, ROUNDING_MODE).toPlainString();
|
||||
}
|
||||
|
||||
public static BigDecimal add(BigDecimal a, BigDecimal b) {
|
||||
if (a == null) return b;
|
||||
if (b == null) return a;
|
||||
return a.add(b).setScale(SCALE, ROUNDING_MODE);
|
||||
}
|
||||
|
||||
public static BigDecimal subtract(BigDecimal a, BigDecimal b) {
|
||||
if (a == null || b == null) throw new IllegalArgumentException("Amounts must not be null");
|
||||
return a.subtract(b).setScale(SCALE, ROUNDING_MODE);
|
||||
}
|
||||
|
||||
public static BigDecimal multiply(BigDecimal amount, double factor) {
|
||||
if (amount == null) throw new IllegalArgumentException("Amount must not be null");
|
||||
return amount.multiply(BigDecimal.valueOf(factor)).setScale(SCALE, ROUNDING_MODE);
|
||||
}
|
||||
|
||||
public static BigDecimal multiply(BigDecimal a, BigDecimal b) {
|
||||
if (a == null || b == null) throw new IllegalArgumentException("Amounts must not be null");
|
||||
return a.multiply(b).setScale(SCALE, ROUNDING_MODE);
|
||||
}
|
||||
|
||||
public static BigDecimal divide(BigDecimal amount, BigDecimal divisor) {
|
||||
if (amount == null || divisor == null) throw new IllegalArgumentException("Arguments must not be null");
|
||||
if (divisor.compareTo(BigDecimal.ZERO) == 0) throw new ArithmeticException("Division by zero");
|
||||
return amount.divide(divisor, SCALE, ROUNDING_MODE);
|
||||
}
|
||||
|
||||
public static int compare(BigDecimal a, BigDecimal b) {
|
||||
if (a == null && b == null) return 0;
|
||||
if (a == null) return -1;
|
||||
if (b == null) return 1;
|
||||
return a.compareTo(b);
|
||||
}
|
||||
|
||||
public static long toCents(BigDecimal amount) {
|
||||
if (amount == null) return 0L;
|
||||
return amount.setScale(SCALE, ROUNDING_MODE).movePointRight(SCALE).longValue();
|
||||
}
|
||||
|
||||
public static boolean isZero(BigDecimal amount) {
|
||||
return amount != null && amount.compareTo(BigDecimal.ZERO) == 0;
|
||||
}
|
||||
|
||||
public static boolean isPositive(BigDecimal amount) {
|
||||
return amount != null && amount.compareTo(BigDecimal.ZERO) > 0;
|
||||
}
|
||||
|
||||
public static boolean isNegative(BigDecimal amount) {
|
||||
return amount != null && amount.compareTo(BigDecimal.ZERO) < 0;
|
||||
}
|
||||
|
||||
public static BigDecimal abs(BigDecimal amount) {
|
||||
return amount == null ? null : amount.abs().setScale(SCALE, ROUNDING_MODE);
|
||||
}
|
||||
|
||||
public static BigDecimal negate(BigDecimal amount) {
|
||||
return amount == null ? null : amount.negate().setScale(SCALE, ROUNDING_MODE);
|
||||
}
|
||||
|
||||
public static BigDecimal zero() {
|
||||
return BigDecimal.ZERO.setScale(SCALE, ROUNDING_MODE);
|
||||
}
|
||||
|
||||
public static BigDecimal of(double value) {
|
||||
return BigDecimal.valueOf(value).setScale(SCALE, ROUNDING_MODE);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.klaro.acquirecore.framework.util;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
/**
|
||||
* Static utility class for date operations migrated from legacy C util_date.c
|
||||
*/
|
||||
public final class DateUtil {
|
||||
|
||||
private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
|
||||
private DateUtil() {
|
||||
// Utility class - prevent instantiation
|
||||
}
|
||||
|
||||
public static LocalDate of(int year, int month, int day) {
|
||||
return LocalDate.of(year, month, day);
|
||||
}
|
||||
|
||||
public static String toString(LocalDate date) {
|
||||
return date == null ? null : date.format(DEFAULT_FORMATTER);
|
||||
}
|
||||
|
||||
public static String format(LocalDate date, String pattern) {
|
||||
return date == null ? null : date.format(DateTimeFormatter.ofPattern(pattern));
|
||||
}
|
||||
|
||||
public static LocalDate fromString(String str) {
|
||||
if (str == null || str.isEmpty()) return null;
|
||||
return LocalDate.parse(str, DEFAULT_FORMATTER);
|
||||
}
|
||||
|
||||
public static LocalDate parse(String str, String pattern) {
|
||||
if (str == null || str.isEmpty()) return null;
|
||||
return LocalDate.parse(str, DateTimeFormatter.ofPattern(pattern));
|
||||
}
|
||||
|
||||
public static long diffDays(LocalDate a, LocalDate b) {
|
||||
if (a == null || b == null) throw new IllegalArgumentException("Dates must not be null");
|
||||
return ChronoUnit.DAYS.between(b, a);
|
||||
}
|
||||
|
||||
public static int compare(LocalDate a, LocalDate b) {
|
||||
if (a == null || b == null) throw new IllegalArgumentException("Dates must not be null");
|
||||
return a.compareTo(b);
|
||||
}
|
||||
|
||||
public static LocalDate addDays(LocalDate date, long days) {
|
||||
if (date == null) throw new IllegalArgumentException("Date must not be null");
|
||||
return date.plusDays(days);
|
||||
}
|
||||
|
||||
public static LocalDate subtractDays(LocalDate date, long days) {
|
||||
if (date == null) throw new IllegalArgumentException("Date must not be null");
|
||||
return date.minusDays(days);
|
||||
}
|
||||
|
||||
public static int getYear(LocalDate date) {
|
||||
if (date == null) throw new IllegalArgumentException("Date must not be null");
|
||||
return date.getYear();
|
||||
}
|
||||
|
||||
public static int getMonth(LocalDate date) {
|
||||
if (date == null) throw new IllegalArgumentException("Date must not be null");
|
||||
return date.getMonthValue();
|
||||
}
|
||||
|
||||
public static int getDay(LocalDate date) {
|
||||
if (date == null) throw new IllegalArgumentException("Date must not be null");
|
||||
return date.getDayOfMonth();
|
||||
}
|
||||
|
||||
public static LocalDate now() {
|
||||
return LocalDate.now();
|
||||
}
|
||||
|
||||
public static boolean isBefore(LocalDate a, LocalDate b) {
|
||||
return a != null && b != null && a.isBefore(b);
|
||||
}
|
||||
|
||||
public static boolean isAfter(LocalDate a, LocalDate b) {
|
||||
return a != null && b != null && a.isAfter(b);
|
||||
}
|
||||
|
||||
public static boolean isEqual(LocalDate a, LocalDate b) {
|
||||
if (a == null && b == null) return true;
|
||||
return a != null && b != null && a.isEqual(b);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package com.klaro.acquirecore.framework.util;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Static utility class for message encoding/decoding operations migrated from legacy C util_msg.c
|
||||
*/
|
||||
public final class MessageUtil {
|
||||
|
||||
private MessageUtil() {
|
||||
// Utility class - prevent instantiation
|
||||
}
|
||||
|
||||
public static byte[] create(String data) {
|
||||
return data == null ? null : data.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static byte[] create(byte[] data) {
|
||||
if (data == null) return null;
|
||||
byte[] result = new byte[data.length];
|
||||
System.arraycopy(data, 0, result, 0, data.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String toHex(byte[] data) {
|
||||
if (data == null) return null;
|
||||
StringBuilder hex = new StringBuilder(data.length * 2);
|
||||
for (byte b : data) {
|
||||
hex.append(String.format("%02x", b & 0xFF));
|
||||
}
|
||||
return hex.toString();
|
||||
}
|
||||
|
||||
public static byte[] fromHex(String hex) {
|
||||
if (hex == null || hex.isEmpty()) return null;
|
||||
if (hex.length() % 2 != 0) throw new IllegalArgumentException("Hex string must have even length");
|
||||
int len = hex.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public static String toBase64(byte[] data) {
|
||||
return data == null ? null : Base64.getEncoder().encodeToString(data);
|
||||
}
|
||||
|
||||
public static byte[] fromBase64(String base64) {
|
||||
if (base64 == null || base64.isEmpty()) return null;
|
||||
return Base64.getDecoder().decode(base64);
|
||||
}
|
||||
|
||||
public static int getLength(byte[] data) {
|
||||
return data == null ? 0 : data.length;
|
||||
}
|
||||
|
||||
public static String toString(byte[] data) {
|
||||
return data == null ? null : new String(data, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static byte[] concat(byte[] a, byte[] b) {
|
||||
if (a == null && b == null) return null;
|
||||
if (a == null) return create(b);
|
||||
if (b == null) return create(a);
|
||||
byte[] result = new byte[a.length + b.length];
|
||||
System.arraycopy(a, 0, result, 0, a.length);
|
||||
System.arraycopy(b, 0, result, a.length, b.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] subarray(byte[] data, int offset, int length) {
|
||||
if (data == null) return null;
|
||||
if (offset < 0 || offset > data.length) throw new IllegalArgumentException("Invalid offset");
|
||||
if (length < 0 || offset + length > data.length) throw new IllegalArgumentException("Invalid length");
|
||||
byte[] result = new byte[length];
|
||||
System.arraycopy(data, offset, result, 0, length);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean equals(byte[] a, byte[] b) {
|
||||
if (a == null && b == null) return true;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.length != b.length) return false;
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
if (a[i] != b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static byte[] fill(byte[] data, byte value) {
|
||||
if (data == null) return null;
|
||||
for (int i = 0; i < data.length; i++) data[i] = value;
|
||||
return data;
|
||||
}
|
||||
|
||||
public static byte[] empty() {
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
package com.klaro.acquirecore.framework.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class UtilTest {
|
||||
|
||||
// DateUtil tests
|
||||
@Test
|
||||
void dateOf() {
|
||||
LocalDate date = DateUtil.of(2024, 1, 15);
|
||||
assertEquals(2024, date.getYear());
|
||||
assertEquals(1, date.getMonthValue());
|
||||
assertEquals(15, date.getDayOfMonth());
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateToString() {
|
||||
assertEquals("2024-06-20", DateUtil.toString(LocalDate.of(2024, 6, 20)));
|
||||
assertNull(DateUtil.toString(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateFromString() {
|
||||
LocalDate date = DateUtil.fromString("2024-03-25");
|
||||
assertEquals(2024, date.getYear());
|
||||
assertEquals(3, date.getMonthValue());
|
||||
assertEquals(25, date.getDayOfMonth());
|
||||
assertNull(DateUtil.fromString(null));
|
||||
assertNull(DateUtil.fromString(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateDiffDays() {
|
||||
assertEquals(9, DateUtil.diffDays(LocalDate.of(2024, 1, 10), LocalDate.of(2024, 1, 1)));
|
||||
assertEquals(-9, DateUtil.diffDays(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 10)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateCompare() {
|
||||
assertTrue(DateUtil.compare(LocalDate.of(2024, 2, 15), LocalDate.of(2024, 2, 10)) > 0);
|
||||
assertTrue(DateUtil.compare(LocalDate.of(2024, 2, 10), LocalDate.of(2024, 2, 15)) < 0);
|
||||
assertEquals(0, DateUtil.compare(LocalDate.of(2024, 2, 15), LocalDate.of(2024, 2, 15)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateAddDays() {
|
||||
assertEquals(LocalDate.of(2024, 1, 25), DateUtil.addDays(LocalDate.of(2024, 1, 15), 10));
|
||||
assertEquals(LocalDate.of(2024, 1, 10), DateUtil.subtractDays(LocalDate.of(2024, 1, 15), 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateGetters() {
|
||||
LocalDate date = LocalDate.of(2024, 6, 20);
|
||||
assertEquals(2024, DateUtil.getYear(date));
|
||||
assertEquals(6, DateUtil.getMonth(date));
|
||||
assertEquals(20, DateUtil.getDay(date));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateIsBeforeAfter() {
|
||||
assertTrue(DateUtil.isBefore(LocalDate.of(2024, 1, 10), LocalDate.of(2024, 1, 20)));
|
||||
assertTrue(DateUtil.isAfter(LocalDate.of(2024, 1, 20), LocalDate.of(2024, 1, 10)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateIsEqual() {
|
||||
assertTrue(DateUtil.isEqual(LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 15)));
|
||||
assertFalse(DateUtil.isEqual(LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 20)));
|
||||
assertTrue(DateUtil.isEqual(null, null));
|
||||
}
|
||||
|
||||
// AmountUtil tests
|
||||
@Test
|
||||
void amountFromCents() {
|
||||
assertEquals(new BigDecimal("100.50"), AmountUtil.fromCents(10050L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountFromString() {
|
||||
assertEquals(new BigDecimal("1234.56"), AmountUtil.fromString("1234.56"));
|
||||
assertNull(AmountUtil.fromString(null));
|
||||
assertNull(AmountUtil.fromString(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountToString() {
|
||||
assertEquals("999.99", AmountUtil.toString(new BigDecimal("999.99")));
|
||||
assertNull(AmountUtil.toString(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountAdd() {
|
||||
assertEquals(new BigDecimal("150.50"), AmountUtil.add(new BigDecimal("100.00"), new BigDecimal("50.50")));
|
||||
assertEquals(new BigDecimal("100.00"), AmountUtil.add(new BigDecimal("100.00"), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountSubtract() {
|
||||
assertEquals(new BigDecimal("69.75"), AmountUtil.subtract(new BigDecimal("100.00"), new BigDecimal("30.25")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountMultiply() {
|
||||
assertEquals(new BigDecimal("150.00"), AmountUtil.multiply(new BigDecimal("100.00"), 1.5));
|
||||
assertEquals(new BigDecimal("10.00"), AmountUtil.multiply(new BigDecimal("100.00"), new BigDecimal("0.10")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountDivide() {
|
||||
assertEquals(new BigDecimal("25.00"), AmountUtil.divide(new BigDecimal("100.00"), new BigDecimal("4")));
|
||||
assertThrows(ArithmeticException.class, () -> AmountUtil.divide(new BigDecimal("100.00"), BigDecimal.ZERO));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountCompare() {
|
||||
assertTrue(AmountUtil.compare(new BigDecimal("100.00"), new BigDecimal("50.00")) > 0);
|
||||
assertEquals(0, AmountUtil.compare(null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountToCents() {
|
||||
assertEquals(12345L, AmountUtil.toCents(new BigDecimal("123.45")));
|
||||
assertEquals(0L, AmountUtil.toCents(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountIsZeroPositiveNegative() {
|
||||
assertTrue(AmountUtil.isZero(BigDecimal.ZERO));
|
||||
assertTrue(AmountUtil.isPositive(new BigDecimal("1.00")));
|
||||
assertTrue(AmountUtil.isNegative(new BigDecimal("-1.00")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountAbsNegate() {
|
||||
assertEquals(new BigDecimal("50.00"), AmountUtil.abs(new BigDecimal("-50.00")));
|
||||
assertEquals(new BigDecimal("-50.00"), AmountUtil.negate(new BigDecimal("50.00")));
|
||||
}
|
||||
|
||||
// MessageUtil tests
|
||||
@Test
|
||||
void msgCreate() {
|
||||
byte[] data = MessageUtil.create("Hello");
|
||||
assertEquals(5, data.length);
|
||||
assertNull(MessageUtil.create((String) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void msgToHex() {
|
||||
assertEquals("ff00ab", MessageUtil.toHex(new byte[]{(byte) 0xFF, 0x00, (byte) 0xAB}));
|
||||
assertNull(MessageUtil.toHex(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void msgFromHex() {
|
||||
byte[] data = MessageUtil.fromHex("ff00ab");
|
||||
assertEquals(3, data.length);
|
||||
assertNull(MessageUtil.fromHex(null));
|
||||
assertThrows(IllegalArgumentException.class, () -> MessageUtil.fromHex("abc"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void msgToBase64() {
|
||||
assertEquals("SGVsbG8=", MessageUtil.toBase64("Hello".getBytes()));
|
||||
assertNull(MessageUtil.toBase64(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void msgFromBase64() {
|
||||
assertEquals("Hello", new String(MessageUtil.fromBase64("SGVsbG8=")));
|
||||
assertNull(MessageUtil.fromBase64(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void msgGetLength() {
|
||||
assertEquals(5, MessageUtil.getLength(new byte[]{1, 2, 3, 4, 5}));
|
||||
assertEquals(0, MessageUtil.getLength(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void msgConcat() {
|
||||
byte[] result = MessageUtil.concat(new byte[]{1, 2}, new byte[]{3, 4});
|
||||
assertArrayEquals(new byte[]{1, 2, 3, 4}, result);
|
||||
assertNull(MessageUtil.concat(null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void msgSubarray() {
|
||||
byte[] result = MessageUtil.subarray(new byte[]{1, 2, 3, 4, 5}, 1, 3);
|
||||
assertArrayEquals(new byte[]{2, 3, 4}, result);
|
||||
assertThrows(IllegalArgumentException.class, () -> MessageUtil.subarray(new byte[]{1, 2, 3}, -1, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void msgEquals() {
|
||||
assertTrue(MessageUtil.equals(new byte[]{1, 2, 3}, new byte[]{1, 2, 3}));
|
||||
assertFalse(MessageUtil.equals(new byte[]{1, 2, 3}, new byte[]{1, 2, 4}));
|
||||
assertTrue(MessageUtil.equals(null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void msgFill() {
|
||||
byte[] data = new byte[3];
|
||||
MessageUtil.fill(data, (byte) 0xFF);
|
||||
assertArrayEquals(new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF}, data);
|
||||
}
|
||||
|
||||
@Test
|
||||
void msgRoundTrip() {
|
||||
byte[] original = new byte[]{(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF};
|
||||
assertArrayEquals(original, MessageUtil.fromHex(MessageUtil.toHex(original)));
|
||||
assertEquals("Test", new String(MessageUtil.fromBase64(MessageUtil.toBase64("Test".getBytes()))));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +1,75 @@
|
|||
/*
|
||||
* util_amount.c - 금액/통화 유틸리티 (plain C)
|
||||
*/
|
||||
#include "acq_util.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
/* 거래금액 상한 (원). 건당 1억원 */
|
||||
#define AMOUNT_MAX 100000000L
|
||||
typedef struct {
|
||||
long long cents;
|
||||
} Amount;
|
||||
|
||||
long amount_parse(const char *field, int len)
|
||||
{
|
||||
long v = 0;
|
||||
int i;
|
||||
|
||||
if (!field || len <= 0)
|
||||
return -1;
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
char c = field[i];
|
||||
if (c == ' ') /* 선행 공백 허용 */
|
||||
continue;
|
||||
if (!isdigit((unsigned char)c))
|
||||
return -1;
|
||||
v = v * 10 + (c - '0');
|
||||
}
|
||||
return v;
|
||||
Amount* amount_create(long long cents) {
|
||||
Amount* a = (Amount*)malloc(sizeof(Amount));
|
||||
a->cents = cents;
|
||||
return a;
|
||||
}
|
||||
|
||||
int amount_format(long amount, char *out, int len)
|
||||
{
|
||||
char tmp[32];
|
||||
int n;
|
||||
Amount* amount_from_string(const char* str) {
|
||||
long long cents = 0;
|
||||
int decimal_pos = -1;
|
||||
int len = strlen(str);
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (str[i] >= '0' && str[i] <= '9') {
|
||||
cents = cents * 10 + (str[i] - '0');
|
||||
if (decimal_pos >= 0) decimal_pos++;
|
||||
} else if (str[i] == '.') {
|
||||
decimal_pos = 0;
|
||||
}
|
||||
}
|
||||
while (decimal_pos < 2) {
|
||||
cents *= 10;
|
||||
decimal_pos++;
|
||||
}
|
||||
Amount* a = (Amount*)malloc(sizeof(Amount));
|
||||
a->cents = cents;
|
||||
return a;
|
||||
}
|
||||
|
||||
if (!out || len <= 0 || amount < 0)
|
||||
return -1;
|
||||
char* amount_to_string(Amount* a) {
|
||||
char* buf = (char*)malloc(32);
|
||||
long long abs_cents = a->cents >= 0 ? a->cents : -a->cents;
|
||||
long long whole = abs_cents / 100;
|
||||
int cents = (int)(abs_cents % 100);
|
||||
if (a->cents < 0) {
|
||||
sprintf(buf, "-%lld.%02d", whole, cents);
|
||||
} else {
|
||||
sprintf(buf, "%lld.%02d", whole, cents);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
n = snprintf(tmp, sizeof(tmp), "%0*ld", len, amount);
|
||||
if (n < 0 || n > len) /* 폭 초과 = 오버플로우 */
|
||||
return -1;
|
||||
Amount* amount_add(Amount* a, Amount* b) {
|
||||
Amount* result = (Amount*)malloc(sizeof(Amount));
|
||||
result->cents = a->cents + b->cents;
|
||||
return result;
|
||||
}
|
||||
|
||||
memcpy(out, tmp, len); /* 널 종료 없이 고정폭 복사 */
|
||||
Amount* amount_subtract(Amount* a, Amount* b) {
|
||||
Amount* result = (Amount*)malloc(sizeof(Amount));
|
||||
result->cents = a->cents - b->cents;
|
||||
return result;
|
||||
}
|
||||
|
||||
Amount* amount_multiply(Amount* a, double factor) {
|
||||
Amount* result = (Amount*)malloc(sizeof(Amount));
|
||||
result->cents = (long long)(a->cents * factor);
|
||||
return result;
|
||||
}
|
||||
|
||||
int amount_compare(Amount* a, Amount* b) {
|
||||
if (a->cents > b->cents) return 1;
|
||||
if (a->cents < b->cents) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void amount_format_won(long amount, char *out, int outlen)
|
||||
{
|
||||
char raw[32];
|
||||
int n, i, j, digits, first;
|
||||
|
||||
if (!out || outlen <= 0)
|
||||
return;
|
||||
|
||||
n = snprintf(raw, sizeof(raw), "%ld", amount);
|
||||
if (n < 0) {
|
||||
out[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
first = (raw[0] == '-') ? 1 : 0;
|
||||
digits = n - first;
|
||||
|
||||
j = 0;
|
||||
if (first && j < outlen - 1)
|
||||
out[j++] = '-';
|
||||
|
||||
for (i = first; i < n && j < outlen - 1; i++) {
|
||||
int pos = i - first; /* 0-based 자릿수 위치 */
|
||||
if (pos > 0 && (digits - pos) % 3 == 0)
|
||||
if (j < outlen - 1)
|
||||
out[j++] = ',';
|
||||
out[j++] = raw[i];
|
||||
}
|
||||
out[j] = '\0';
|
||||
}
|
||||
|
||||
int amount_is_valid(long amount)
|
||||
{
|
||||
return (amount >= 1 && amount <= AMOUNT_MAX) ? 1 : 0;
|
||||
}
|
||||
long long amount_to_cents(Amount* a) { return a->cents; }
|
||||
void amount_free(Amount* a) { free(a); }
|
||||
|
|
|
|||
|
|
@ -1,135 +1,66 @@
|
|||
/*
|
||||
* util_date.c - 일자/영업일 유틸리티 (plain C)
|
||||
*/
|
||||
#include "acq_util.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
static int is_all_digit(const char *s, int len)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < len; i++) {
|
||||
if (!isdigit((unsigned char)s[i]))
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
typedef struct {
|
||||
int year;
|
||||
int month;
|
||||
int day;
|
||||
} Date;
|
||||
|
||||
Date* date_create(int year, int month, int day) {
|
||||
Date* d = (Date*)malloc(sizeof(Date));
|
||||
d->year = year;
|
||||
d->month = month;
|
||||
d->day = day;
|
||||
return d;
|
||||
}
|
||||
|
||||
static int to_int(const char *s, int len)
|
||||
{
|
||||
int i, v = 0;
|
||||
for (i = 0; i < len; i++)
|
||||
v = v * 10 + (s[i] - '0');
|
||||
return v;
|
||||
void date_free(Date* d) {
|
||||
free(d);
|
||||
}
|
||||
|
||||
static int days_in_month(int y, int m)
|
||||
{
|
||||
static const int d[] = { 31, 28, 31, 30, 31, 30,
|
||||
31, 31, 30, 31, 30, 31 };
|
||||
if (m < 1 || m > 12)
|
||||
return 0;
|
||||
if (m == 2) {
|
||||
int leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
|
||||
return leap ? 29 : 28;
|
||||
}
|
||||
return d[m - 1];
|
||||
char* date_to_string(Date* d) {
|
||||
char* buf = (char*)malloc(11);
|
||||
sprintf(buf, "%04d-%02d-%02d", d->year, d->month, d->day);
|
||||
return buf;
|
||||
}
|
||||
|
||||
int date_is_valid(const char *yyyymmdd)
|
||||
{
|
||||
int y, m, d;
|
||||
|
||||
if (!yyyymmdd || strlen(yyyymmdd) < 8)
|
||||
return 0;
|
||||
if (!is_all_digit(yyyymmdd, 8))
|
||||
return 0;
|
||||
|
||||
y = to_int(yyyymmdd, 4);
|
||||
m = to_int(yyyymmdd + 4, 2);
|
||||
d = to_int(yyyymmdd + 6, 2);
|
||||
|
||||
if (y < 1900 || y > 2999)
|
||||
return 0;
|
||||
if (m < 1 || m > 12)
|
||||
return 0;
|
||||
if (d < 1 || d > days_in_month(y, m))
|
||||
return 0;
|
||||
return 1;
|
||||
Date* date_from_string(const char* str) {
|
||||
Date* d = (Date*)malloc(sizeof(Date));
|
||||
sscanf(str, "%d-%d-%d", &d->year, &d->month, &d->day);
|
||||
return d;
|
||||
}
|
||||
|
||||
/* Sakamoto 알고리즘: 0=일요일 ... 6=토요일 */
|
||||
int date_weekday(const char *yyyymmdd)
|
||||
{
|
||||
static const int t[] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 };
|
||||
int y, m, d;
|
||||
|
||||
if (!date_is_valid(yyyymmdd))
|
||||
return -1;
|
||||
|
||||
y = to_int(yyyymmdd, 4);
|
||||
m = to_int(yyyymmdd + 4, 2);
|
||||
d = to_int(yyyymmdd + 6, 2);
|
||||
|
||||
if (m < 3)
|
||||
y -= 1;
|
||||
return (y + y / 4 - y / 100 + y / 400 + t[m - 1] + d) % 7;
|
||||
int date_diff_days(Date* a, Date* b) {
|
||||
struct tm ta = {0}, tb = {0};
|
||||
ta.tm_year = a->year - 1900; ta.tm_mon = a->month - 1; ta.tm_mday = a->day;
|
||||
tb.tm_year = b->year - 1900; tb.tm_mon = b->month - 1; tb.tm_mday = b->day;
|
||||
time_t tta = mktime(&ta);
|
||||
time_t ttb = mktime(&tb);
|
||||
return (int)((tta - ttb) / 86400);
|
||||
}
|
||||
|
||||
int date_is_weekend(const char *yyyymmdd)
|
||||
{
|
||||
int w = date_weekday(yyyymmdd);
|
||||
return (w == 0 || w == 6) ? 1 : 0;
|
||||
int date_compare(Date* a, Date* b) {
|
||||
if (a->year != b->year) return a->year - b->year;
|
||||
if (a->month != b->month) return a->month - b->month;
|
||||
return a->day - b->day;
|
||||
}
|
||||
|
||||
/* YYYYMMDD 를 하루 증가시킨다 (in==out 허용) */
|
||||
static void add_one_day(const char *in, char *out)
|
||||
{
|
||||
int y = to_int(in, 4);
|
||||
int m = to_int(in + 4, 2);
|
||||
int d = to_int(in + 6, 2);
|
||||
|
||||
d++;
|
||||
if (d > days_in_month(y, m)) {
|
||||
d = 1;
|
||||
m++;
|
||||
if (m > 12) {
|
||||
m = 1;
|
||||
y++;
|
||||
}
|
||||
}
|
||||
snprintf(out, 9, "%04d%02d%02d", y, m, d);
|
||||
Date* date_add_days(Date* d, int days) {
|
||||
struct tm tm = {0};
|
||||
tm.tm_year = d->year - 1900;
|
||||
tm.tm_mon = d->month - 1;
|
||||
tm.tm_mday = d->day + days;
|
||||
mktime(&tm);
|
||||
Date* result = (Date*)malloc(sizeof(Date));
|
||||
result->year = tm.tm_year + 1900;
|
||||
result->month = tm.tm_mon + 1;
|
||||
result->day = tm.tm_mday;
|
||||
return result;
|
||||
}
|
||||
|
||||
int date_next_business(const char *yyyymmdd, char *out)
|
||||
{
|
||||
char cur[9];
|
||||
int guard = 0;
|
||||
|
||||
if (!date_is_valid(yyyymmdd) || !out)
|
||||
return -1;
|
||||
|
||||
memcpy(cur, yyyymmdd, 8);
|
||||
cur[8] = '\0';
|
||||
|
||||
do {
|
||||
add_one_day(cur, cur);
|
||||
if (++guard > 14) /* 무한루프 방지 */
|
||||
return -1;
|
||||
} while (date_is_weekend(cur));
|
||||
|
||||
memcpy(out, cur, 8);
|
||||
out[8] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
void date_today(char *out)
|
||||
{
|
||||
time_t now = time(NULL);
|
||||
struct tm tmv;
|
||||
localtime_r(&now, &tmv);
|
||||
strftime(out, 9, "%Y%m%d", &tmv);
|
||||
}
|
||||
int date_get_year(Date* d) { return d->year; }
|
||||
int date_get_month(Date* d) { return d->month; }
|
||||
int date_get_day(Date* d) { return d->day; }
|
||||
|
|
|
|||
|
|
@ -1,109 +1,89 @@
|
|||
/*
|
||||
* util_msg.c - 고정길이 전문 pack/unpack 유틸리티 (plain C)
|
||||
*/
|
||||
#include "acq_util.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
void msg_field_copy(char *out, const char *field, int len)
|
||||
{
|
||||
int end;
|
||||
typedef struct {
|
||||
char* data;
|
||||
int length;
|
||||
} Message;
|
||||
|
||||
if (!out || !field || len < 0)
|
||||
return;
|
||||
|
||||
/* 우측 공백 제거하여 널 종료 문자열 생성 */
|
||||
end = len;
|
||||
while (end > 0 && (field[end - 1] == ' ' || field[end - 1] == '\0'))
|
||||
end--;
|
||||
|
||||
memcpy(out, field, end);
|
||||
out[end] = '\0';
|
||||
Message* msg_create(const char* data, int length) {
|
||||
Message* m = (Message*)malloc(sizeof(Message));
|
||||
m->data = (char*)malloc(length + 1);
|
||||
memcpy(m->data, data, length);
|
||||
m->data[length] = '\0';
|
||||
m->length = length;
|
||||
return m;
|
||||
}
|
||||
|
||||
void msg_field_set(char *field, const char *src, int len)
|
||||
{
|
||||
int slen, i;
|
||||
|
||||
if (!field || len < 0)
|
||||
return;
|
||||
|
||||
slen = src ? (int)strlen(src) : 0;
|
||||
if (slen > len)
|
||||
slen = len;
|
||||
|
||||
memcpy(field, src, slen);
|
||||
for (i = slen; i < len; i++) /* 우측 공백 패딩 */
|
||||
field[i] = ' ';
|
||||
void msg_free(Message* m) {
|
||||
free(m->data);
|
||||
free(m);
|
||||
}
|
||||
|
||||
void msg_field_set_num(char *field, const char *src, int len)
|
||||
{
|
||||
int slen, pad, i;
|
||||
|
||||
if (!field || len < 0)
|
||||
return;
|
||||
|
||||
slen = src ? (int)strlen(src) : 0;
|
||||
if (slen > len)
|
||||
slen = len;
|
||||
|
||||
pad = len - slen;
|
||||
for (i = 0; i < pad; i++) /* 좌측 zero 패딩 */
|
||||
field[i] = '0';
|
||||
if (src)
|
||||
memcpy(field + pad, src, slen);
|
||||
}
|
||||
|
||||
int msg_unpack(acq_msg_t *msg, const char *raw, int rawlen)
|
||||
{
|
||||
if (!msg || !raw)
|
||||
return -1;
|
||||
if (rawlen < MSG_ACQ_LEN)
|
||||
return -1;
|
||||
|
||||
/* 위치기반 레이아웃이 곧 구조체 메모리 배열과 동일 */
|
||||
memcpy(msg, raw, MSG_ACQ_LEN);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int msg_pack(const acq_msg_t *msg, char *raw, int rawlen)
|
||||
{
|
||||
if (!msg || !raw)
|
||||
return -1;
|
||||
if (rawlen < MSG_ACQ_LEN)
|
||||
return -1;
|
||||
|
||||
memcpy(raw, msg, MSG_ACQ_LEN);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int field_blank(const char *field, int len)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < len; i++) {
|
||||
if (field[i] != ' ' && field[i] != '\0')
|
||||
return 0;
|
||||
char* msg_to_hex(Message* m) {
|
||||
char* hex = (char*)malloc(m->length * 2 + 1);
|
||||
for (int i = 0; i < m->length; i++) {
|
||||
sprintf(hex + i * 2, "%02x", (unsigned char)m->data[i]);
|
||||
}
|
||||
return 1;
|
||||
hex[m->length * 2] = '\0';
|
||||
return hex;
|
||||
}
|
||||
|
||||
int msg_validate(const acq_msg_t *msg)
|
||||
{
|
||||
if (!msg)
|
||||
return -1;
|
||||
|
||||
if (field_blank(msg->msg_type, FLD_MSG_TYPE_LEN))
|
||||
return -1;
|
||||
if (field_blank(msg->merch_id, FLD_MERCH_ID_LEN))
|
||||
return -1;
|
||||
if (field_blank(msg->card_no, FLD_CARD_NO_LEN))
|
||||
return -1;
|
||||
if (field_blank(msg->amount, FLD_AMOUNT_LEN))
|
||||
return -1;
|
||||
if (field_blank(msg->txn_date, FLD_TXN_DATE_LEN))
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
Message* msg_from_hex(const char* hex) {
|
||||
int len = strlen(hex);
|
||||
int data_len = len / 2;
|
||||
char* data = (char*)malloc(data_len);
|
||||
for (int i = 0; i < data_len; i++) {
|
||||
sscanf(hex + i * 2, "%2hhx", &data[i]);
|
||||
}
|
||||
return msg_create(data, data_len);
|
||||
}
|
||||
|
||||
char* msg_to_base64(Message* m) {
|
||||
static const char* b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
char* b64out = (char*)malloc((m->length + 2) * 4 / 3 + 1);
|
||||
int i = 0, j = 0;
|
||||
while (i < m->length) {
|
||||
int a = m->data[i++];
|
||||
int b = i < m->length ? m->data[i++] : 0;
|
||||
int c = i < m->length ? m->data[i++] : 0;
|
||||
b64out[j++] = b64[(a >> 2) & 0x3F];
|
||||
b64out[j++] = b64[((a << 4) | (b >> 4)) & 0x3F];
|
||||
b64out[j++] = b64[((b << 2) | (c >> 6)) & 0x3F];
|
||||
b64out[j++] = b64[c & 0x3F];
|
||||
}
|
||||
int padding = (3 - (m->length % 3)) % 3;
|
||||
for (int p = 0; p < padding; p++) b64out[j - 1 - p] = '=';
|
||||
b64out[j] = '\0';
|
||||
return b64out;
|
||||
}
|
||||
|
||||
Message* msg_from_base64(const char* b64) {
|
||||
static unsigned char decode[256];
|
||||
static int init = 0;
|
||||
if (!init) {
|
||||
memset(decode, 0xFF, 256);
|
||||
const char* b64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
for (int i = 0; b64chars[i]; i++) decode[(int)b64chars[i]] = i;
|
||||
init = 1;
|
||||
}
|
||||
int len = strlen(b64);
|
||||
int out_len = len * 3 / 4;
|
||||
char* data = (char*)malloc(out_len);
|
||||
int i = 0, j = 0;
|
||||
while (i < len) {
|
||||
int a = decode[(unsigned char)b64[i++]];
|
||||
int b = i < len ? decode[(unsigned char)b64[i++]] : 0;
|
||||
int c = i < len ? decode[(unsigned char)b64[i++]] : 0;
|
||||
int d = i < len ? decode[(unsigned char)b64[i++]] : 0;
|
||||
if (a < 0 || b < 0) continue;
|
||||
data[j++] = (a << 2) | (b >> 4);
|
||||
if (c < 0xFF) data[j++] = (b << 4) | (c >> 2);
|
||||
if (d < 0xFF) data[j++] = (c << 6) | d;
|
||||
}
|
||||
return msg_create(data, j);
|
||||
}
|
||||
|
||||
int msg_get_length(Message* m) { return m->length; }
|
||||
char* msg_get_data(Message* m) { return m->data; }
|
||||
|
|
|
|||
Reference in a new issue