[공통] util → framework 유틸 #7

Open
forge-bot wants to merge 8 commits from forge/ACM-CM-002-attempt-3-run-d48451005e39 into main
8 changed files with 700 additions and 287 deletions

View file

@ -0,0 +1,3 @@
# ACM-CM-002-attempt-3-run-d48451005e39
Forge 이슈 작업 브랜치 `forge/ACM-CM-002-attempt-3-run-d48451005e39`.

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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];
}
}

View file

@ -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()))));
}
}

View file

@ -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); }

View file

@ -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; }

View file

@ -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; }