[공통] util → framework 유틸 #5
8 changed files with 644 additions and 285 deletions
3
.forge/ACM-CM-002-attempt-2-run-87bf2707792a.md
Normal file
3
.forge/ACM-CM-002-attempt-2-run-87bf2707792a.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# ACM-CM-002-attempt-2-run-87bf2707792a
|
||||
|
||||
Forge 이슈 작업 브랜치 `forge/ACM-CM-002-attempt-2-run-87bf2707792a`.
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.klaro.acquirecore.framework.util;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
/**
|
||||
* Static utility class for monetary amount operations migrated from legacy C code.
|
||||
* Provides parsing, formatting, and arithmetic for BigDecimal amounts.
|
||||
*/
|
||||
public final class AmountUtil {
|
||||
|
||||
public static final String DEFAULT_CURRENCY = "KRW";
|
||||
private static final int DEFAULT_SCALE = 2;
|
||||
private static final RoundingMode DEFAULT_ROUNDING = RoundingMode.HALF_UP;
|
||||
|
||||
private AmountUtil() {
|
||||
// Utility class - prevent instantiation
|
||||
}
|
||||
|
||||
public static BigDecimal parse(String str) {
|
||||
if (str == null || str.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String cleaned = str.trim().replaceAll(",", "");
|
||||
cleaned = cleaned.replaceAll("[A-Z]{3}\\s*", "");
|
||||
return new BigDecimal(cleaned).setScale(DEFAULT_SCALE, DEFAULT_ROUNDING);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String format(BigDecimal amount, String currency) {
|
||||
if (amount == null) {
|
||||
return "";
|
||||
}
|
||||
String curr = (currency != null && !currency.isBlank()) ? currency : DEFAULT_CURRENCY;
|
||||
return String.format("%s %s", curr, amount.setScale(DEFAULT_SCALE, DEFAULT_ROUNDING).toPlainString());
|
||||
}
|
||||
|
||||
public static String format(BigDecimal amount) {
|
||||
return format(amount, DEFAULT_CURRENCY);
|
||||
}
|
||||
|
||||
public static BigDecimal add(BigDecimal a, BigDecimal b) {
|
||||
if (a == null || b == null) {
|
||||
return null;
|
||||
}
|
||||
return a.add(b).setScale(DEFAULT_SCALE, DEFAULT_ROUNDING);
|
||||
}
|
||||
|
||||
public static BigDecimal subtract(BigDecimal a, BigDecimal b) {
|
||||
if (a == null || b == null) {
|
||||
return null;
|
||||
}
|
||||
return a.subtract(b).setScale(DEFAULT_SCALE, DEFAULT_ROUNDING);
|
||||
}
|
||||
|
||||
public static BigDecimal multiply(BigDecimal amount, int factor) {
|
||||
if (amount == null) {
|
||||
return null;
|
||||
}
|
||||
return amount.multiply(BigDecimal.valueOf(factor)).setScale(DEFAULT_SCALE, DEFAULT_ROUNDING);
|
||||
}
|
||||
|
||||
public static BigDecimal multiply(BigDecimal amount, BigDecimal factor) {
|
||||
if (amount == null || factor == null) {
|
||||
return null;
|
||||
}
|
||||
return amount.multiply(factor).setScale(DEFAULT_SCALE, DEFAULT_ROUNDING);
|
||||
}
|
||||
|
||||
public static BigDecimal divide(BigDecimal amount, BigDecimal divisor) {
|
||||
if (amount == null || divisor == null || divisor.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return null;
|
||||
}
|
||||
return amount.divide(divisor, DEFAULT_SCALE, DEFAULT_ROUNDING);
|
||||
}
|
||||
|
||||
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 boolean isZero(BigDecimal amount) {
|
||||
return amount != null && amount.compareTo(BigDecimal.ZERO) == 0;
|
||||
}
|
||||
|
||||
public static BigDecimal abs(BigDecimal amount) {
|
||||
if (amount == null) {
|
||||
return null;
|
||||
}
|
||||
return amount.abs().setScale(DEFAULT_SCALE, DEFAULT_ROUNDING);
|
||||
}
|
||||
|
||||
public static BigDecimal negate(BigDecimal amount) {
|
||||
if (amount == null) {
|
||||
return null;
|
||||
}
|
||||
return amount.negate().setScale(DEFAULT_SCALE, DEFAULT_ROUNDING);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.klaro.acquirecore.framework.util;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
/**
|
||||
* Static utility class for date operations migrated from legacy C code.
|
||||
* Provides parsing, formatting, and calculation for LocalDate instances.
|
||||
*/
|
||||
public final class DateUtil {
|
||||
|
||||
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
||||
private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT);
|
||||
|
||||
private DateUtil() {
|
||||
// Utility class - prevent instantiation
|
||||
}
|
||||
|
||||
public static LocalDate parse(String dateStr) {
|
||||
if (dateStr == null || dateStr.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(dateStr.trim(), DEFAULT_FORMATTER);
|
||||
} catch (DateTimeParseException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String format(LocalDate date) {
|
||||
if (date == null) {
|
||||
return "";
|
||||
}
|
||||
return date.format(DEFAULT_FORMATTER);
|
||||
}
|
||||
|
||||
public static String format(LocalDate date, String pattern) {
|
||||
if (date == null || pattern == null || pattern.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return date.format(DateTimeFormatter.ofPattern(pattern));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static long diffDays(LocalDate from, LocalDate to) {
|
||||
if (from == null || to == null) {
|
||||
return 0;
|
||||
}
|
||||
return ChronoUnit.DAYS.between(from, to);
|
||||
}
|
||||
|
||||
public static LocalDate addDays(LocalDate date, long days) {
|
||||
if (date == null) {
|
||||
return null;
|
||||
}
|
||||
return date.plusDays(days);
|
||||
}
|
||||
|
||||
public static LocalDate today() {
|
||||
return LocalDate.now();
|
||||
}
|
||||
|
||||
public static boolean isValid(String dateStr) {
|
||||
return parse(dateStr) != null;
|
||||
}
|
||||
|
||||
public static boolean isBefore(LocalDate date, LocalDate before) {
|
||||
if (date == null || before == null) {
|
||||
return false;
|
||||
}
|
||||
return date.isBefore(before);
|
||||
}
|
||||
|
||||
public static boolean isAfter(LocalDate date, LocalDate after) {
|
||||
if (date == null || after == null) {
|
||||
return false;
|
||||
}
|
||||
return date.isAfter(after);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
package com.klaro.acquirecore.framework.util;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Static utility class for message/frame processing migrated from legacy C code.
|
||||
* Provides encoding, decoding, and checksum operations for message frames.
|
||||
*/
|
||||
public final class MessageUtil {
|
||||
|
||||
private static final int HEADER_SIZE = 4;
|
||||
private static final int MAX_MESSAGE_SIZE = 4096;
|
||||
private static final String DELIMITER = "|";
|
||||
|
||||
private MessageUtil() {
|
||||
// Utility class - prevent instantiation
|
||||
}
|
||||
|
||||
public static String encode(String type, byte[] payload) {
|
||||
if (type == null || type.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
int length = (payload != null) ? payload.length : 0;
|
||||
String header = String.format("%s%s%d%s", padRight(type, HEADER_SIZE), DELIMITER, length, DELIMITER);
|
||||
if (payload == null || payload.length == 0) {
|
||||
return header;
|
||||
}
|
||||
return header + new String(payload, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static String encode(String type, String payload) {
|
||||
if (payload == null) {
|
||||
return encode(type, (byte[]) null);
|
||||
}
|
||||
return encode(type, payload.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static MessageFrame decode(String message) {
|
||||
if (message == null || message.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
int firstDelim = message.indexOf(DELIMITER);
|
||||
if (firstDelim < 0 || firstDelim > HEADER_SIZE) {
|
||||
return null;
|
||||
}
|
||||
String type = message.substring(0, firstDelim).trim();
|
||||
int secondDelim = message.indexOf(DELIMITER, firstDelim + 1);
|
||||
if (secondDelim < 0) {
|
||||
return null;
|
||||
}
|
||||
String lengthStr = message.substring(firstDelim + 1, secondDelim).trim();
|
||||
int length;
|
||||
try {
|
||||
length = Integer.parseInt(lengthStr);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
String payloadStr = message.substring(secondDelim + 1);
|
||||
byte[] payload = payloadStr.getBytes(StandardCharsets.UTF_8);
|
||||
return new MessageFrame(type, length, payload);
|
||||
}
|
||||
|
||||
public static int checksum(byte[] payload) {
|
||||
if (payload == null || payload.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
int sum = 0;
|
||||
for (byte b : payload) {
|
||||
sum += (b & 0xFF);
|
||||
}
|
||||
return sum & 0xFFFF;
|
||||
}
|
||||
|
||||
public static int checksum(String payload) {
|
||||
if (payload == null || payload.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return checksum(payload.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static String encodeBase64(byte[] data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(data);
|
||||
}
|
||||
|
||||
public static byte[] decodeBase64(String encoded) {
|
||||
if (encoded == null || encoded.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Base64.getDecoder().decode(encoded);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isValidSize(String message) {
|
||||
return message != null && message.length() <= MAX_MESSAGE_SIZE;
|
||||
}
|
||||
|
||||
private static String padRight(String str, int length) {
|
||||
if (str == null) {
|
||||
str = "";
|
||||
}
|
||||
if (str.length() >= length) {
|
||||
return str.substring(0, length);
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(str);
|
||||
while (sb.length() < length) {
|
||||
sb.append(' ');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static final class MessageFrame {
|
||||
private final String type;
|
||||
private final int length;
|
||||
private final byte[] payload;
|
||||
|
||||
public MessageFrame(String type, int length, byte[] payload) {
|
||||
this.type = type;
|
||||
this.length = length;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public String getType() { return type; }
|
||||
public int getLength() { return length; }
|
||||
public byte[] getPayload() { return payload; }
|
||||
|
||||
public String getPayloadAsString() {
|
||||
if (payload == null) {
|
||||
return "";
|
||||
}
|
||||
return new String(payload, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("MessageFrame{type='%s', length=%d, payload=%s}", type, length, getPayloadAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
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 FrameworkUtilTest {
|
||||
|
||||
// DateUtil Tests
|
||||
@Test
|
||||
void dateUtil_parse_ValidDate() {
|
||||
LocalDate result = DateUtil.parse("2024-01-15");
|
||||
assertNotNull(result);
|
||||
assertEquals(2024, result.getYear());
|
||||
assertEquals(1, result.getMonthValue());
|
||||
assertEquals(15, result.getDayOfMonth());
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateUtil_parse_InvalidDate() {
|
||||
assertNull(DateUtil.parse("invalid"));
|
||||
assertNull(DateUtil.parse(null));
|
||||
assertNull(DateUtil.parse(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateUtil_format() {
|
||||
assertEquals("2024-03-20", DateUtil.format(LocalDate.of(2024, 3, 20)));
|
||||
assertEquals("", DateUtil.format((LocalDate) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateUtil_diffDays() {
|
||||
assertEquals(10, DateUtil.diffDays(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 11)));
|
||||
assertEquals(-5, DateUtil.diffDays(LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 10)));
|
||||
assertEquals(0, DateUtil.diffDays(null, LocalDate.now()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateUtil_addDays() {
|
||||
assertEquals(LocalDate.of(2024, 1, 20), DateUtil.addDays(LocalDate.of(2024, 1, 15), 5));
|
||||
assertEquals(LocalDate.of(2024, 1, 5), DateUtil.addDays(LocalDate.of(2024, 1, 15), -10));
|
||||
assertNull(DateUtil.addDays(null, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateUtil_today() {
|
||||
assertEquals(LocalDate.now(), DateUtil.today());
|
||||
}
|
||||
|
||||
// AmountUtil Tests
|
||||
@Test
|
||||
void amountUtil_parse_ValidAmount() {
|
||||
assertEquals(new BigDecimal("1000.00"), AmountUtil.parse("1000"));
|
||||
assertEquals(new BigDecimal("1000.50"), AmountUtil.parse("1000.50"));
|
||||
assertEquals(new BigDecimal("-500.25"), AmountUtil.parse("-500.25"));
|
||||
assertEquals(new BigDecimal("1000.00"), AmountUtil.parse("KRW 1000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountUtil_parse_InvalidAmount() {
|
||||
assertNull(AmountUtil.parse("invalid"));
|
||||
assertNull(AmountUtil.parse(null));
|
||||
assertNull(AmountUtil.parse(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountUtil_format() {
|
||||
assertEquals("KRW 1000.50", AmountUtil.format(new BigDecimal("1000.50")));
|
||||
assertEquals("USD 1000.50", AmountUtil.format(new BigDecimal("1000.50"), "USD"));
|
||||
assertEquals("", AmountUtil.format(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountUtil_add() {
|
||||
assertEquals(new BigDecimal("150.75"), AmountUtil.add(new BigDecimal("100.50"), new BigDecimal("50.25")));
|
||||
assertNull(AmountUtil.add(null, new BigDecimal("10")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountUtil_subtract() {
|
||||
assertEquals(new BigDecimal("50.25"), AmountUtil.subtract(new BigDecimal("100.50"), new BigDecimal("50.25")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountUtil_multiply() {
|
||||
assertEquals(new BigDecimal("301.50"), AmountUtil.multiply(new BigDecimal("100.50"), 3));
|
||||
assertEquals(new BigDecimal("150.00"), AmountUtil.multiply(new BigDecimal("100.00"), new BigDecimal("1.5")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountUtil_divide() {
|
||||
assertEquals(new BigDecimal("25.00"), AmountUtil.divide(new BigDecimal("100.00"), new BigDecimal("4")));
|
||||
assertNull(AmountUtil.divide(new BigDecimal("100"), BigDecimal.ZERO));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountUtil_isPositive() {
|
||||
assertTrue(AmountUtil.isPositive(new BigDecimal("100")));
|
||||
assertFalse(AmountUtil.isPositive(new BigDecimal("-100")));
|
||||
assertFalse(AmountUtil.isPositive(BigDecimal.ZERO));
|
||||
}
|
||||
|
||||
// MessageUtil Tests
|
||||
@Test
|
||||
void messageUtil_encode_ByteArray() {
|
||||
String result = MessageUtil.encode("TEST", "Hello World".getBytes());
|
||||
assertNotNull(result);
|
||||
assertTrue(result.startsWith("TEST|11|"));
|
||||
assertTrue(result.endsWith("Hello World"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageUtil_encode_NullType() {
|
||||
assertNull(MessageUtil.encode(null, "payload"));
|
||||
assertNull(MessageUtil.encode("", "payload"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageUtil_decode_ValidMessage() {
|
||||
MessageUtil.MessageFrame frame = MessageUtil.decode("TEST|11|Hello World");
|
||||
assertNotNull(frame);
|
||||
assertEquals("TEST", frame.getType());
|
||||
assertEquals(11, frame.getLength());
|
||||
assertEquals("Hello World", frame.getPayloadAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageUtil_decode_InvalidMessage() {
|
||||
assertNull(MessageUtil.decode(null));
|
||||
assertNull(MessageUtil.decode(""));
|
||||
assertNull(MessageUtil.decode("invalid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageUtil_checksum() {
|
||||
assertEquals(198, MessageUtil.checksum("ABC".getBytes()));
|
||||
assertEquals(0, MessageUtil.checksum((byte[]) null));
|
||||
assertEquals(198, MessageUtil.checksum("ABC"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageUtil_base64() {
|
||||
assertEquals("SGVsbG8=", MessageUtil.encodeBase64("Hello".getBytes()));
|
||||
assertEquals("Hello", new String(MessageUtil.decodeBase64("SGVsbG8=")));
|
||||
assertNull(MessageUtil.decodeBase64("!!!invalid!!!"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageUtil_roundTrip() {
|
||||
String type = "DATA";
|
||||
String payload = "Round trip test";
|
||||
String encoded = MessageUtil.encode(type, payload);
|
||||
MessageUtil.MessageFrame decoded = MessageUtil.decode(encoded);
|
||||
assertNotNull(decoded);
|
||||
assertEquals(type, decoded.getType());
|
||||
assertEquals(payload, decoded.getPayloadAsString());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +1,61 @@
|
|||
/*
|
||||
* util_amount.c - 금액/통화 유틸리티 (plain C)
|
||||
*/
|
||||
#include "acq_util.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* 거래금액 상한 (원). 건당 1억원 */
|
||||
#define AMOUNT_MAX 100000000L
|
||||
#define AMOUNT_BUF_SIZE 64
|
||||
|
||||
long amount_parse(const char *field, int len)
|
||||
{
|
||||
long v = 0;
|
||||
int i;
|
||||
typedef struct {
|
||||
long long units;
|
||||
int cents;
|
||||
char currency[4];
|
||||
} amount_t;
|
||||
|
||||
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');
|
||||
int amount_parse(const char* str, amount_t* out) {
|
||||
if (!str || !out) return -1;
|
||||
long long units = 0;
|
||||
int cents = 0;
|
||||
char currency[4] = "KRW";
|
||||
const char* p = str;
|
||||
while (*p && !isdigit(*p) && *p != '.' && *p != '-') p++;
|
||||
if (sscanf(p, "%lld.%2d", &units, ¢s) >= 1) {
|
||||
out->units = units;
|
||||
out->cents = cents;
|
||||
strcpy(out->currency, currency);
|
||||
return 0;
|
||||
}
|
||||
return v;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int amount_format(long amount, char *out, int len)
|
||||
{
|
||||
char tmp[32];
|
||||
int n;
|
||||
int amount_format(const amount_t* a, char* buf, size_t len) {
|
||||
if (!a || !buf) return -1;
|
||||
return snprintf(buf, len, "%s %lld.%02d", a->currency, a->units, a->cents);
|
||||
}
|
||||
|
||||
if (!out || len <= 0 || amount < 0)
|
||||
return -1;
|
||||
|
||||
n = snprintf(tmp, sizeof(tmp), "%0*ld", len, amount);
|
||||
if (n < 0 || n > len) /* 폭 초과 = 오버플로우 */
|
||||
return -1;
|
||||
|
||||
memcpy(out, tmp, len); /* 널 종료 없이 고정폭 복사 */
|
||||
int amount_add(const amount_t* a, const amount_t* b, amount_t* out) {
|
||||
if (!a || !b || !out) return -1;
|
||||
long long total_cents = a->units * 100 + a->cents + b->units * 100 + b->cents;
|
||||
out->units = total_cents / 100;
|
||||
out->cents = (int)(total_cents % 100);
|
||||
strcpy(out->currency, a->currency);
|
||||
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_subtract(const amount_t* a, const amount_t* b, amount_t* out) {
|
||||
if (!a || !b || !out) return -1;
|
||||
long long total_cents = (a->units * 100 + a->cents) - (b->units * 100 + b->cents);
|
||||
out->units = total_cents / 100;
|
||||
out->cents = (int)(total_cents % 100);
|
||||
if (total_cents < 0) out->cents = -out->cents;
|
||||
strcpy(out->currency, a->currency);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int amount_is_valid(long amount)
|
||||
{
|
||||
return (amount >= 1 && amount <= AMOUNT_MAX) ? 1 : 0;
|
||||
int amount_multiply(const amount_t* a, int factor, amount_t* out) {
|
||||
if (!a || !out) return -1;
|
||||
long long total_cents = (a->units * 100 + a->cents) * factor;
|
||||
out->units = total_cents / 100;
|
||||
out->cents = (int)(total_cents % 100);
|
||||
strcpy(out->currency, a->currency);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,135 +1,52 @@
|
|||
/*
|
||||
* 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;
|
||||
#define DATE_BUF_SIZE 32
|
||||
|
||||
typedef struct {
|
||||
int year;
|
||||
int month;
|
||||
int day;
|
||||
} date_t;
|
||||
|
||||
int date_parse(const char* str, date_t* out) {
|
||||
if (!str || !out) return -1;
|
||||
return sscanf(str, "%4d-%2d-%2d", &out->year, &out->month, &out->day);
|
||||
}
|
||||
|
||||
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;
|
||||
int date_format(const date_t* d, char* buf, size_t len) {
|
||||
if (!d || !buf) return -1;
|
||||
return snprintf(buf, len, "%04d-%02d-%02d", d->year, d->month, d->day);
|
||||
}
|
||||
|
||||
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];
|
||||
int date_diff_days(const date_t* from, const date_t* to) {
|
||||
if (!from || !to) return 0;
|
||||
struct tm a = {0}, b = {0};
|
||||
a.tm_year = from->year - 1900; a.tm_mon = from->month - 1; a.tm_mday = from->day;
|
||||
b.tm_year = to->year - 1900; b.tm_mon = to->month - 1; b.tm_mday = to->day;
|
||||
return (int)(difftime(mktime(&b), mktime(&a)) / 86400);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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_is_weekend(const char *yyyymmdd)
|
||||
{
|
||||
int w = date_weekday(yyyymmdd);
|
||||
return (w == 0 || w == 6) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
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';
|
||||
int date_add_days(const date_t* d, int days, date_t* out) {
|
||||
if (!d || !out) return -1;
|
||||
struct tm base = {0};
|
||||
base.tm_year = d->year - 1900; base.tm_mon = d->month - 1; base.tm_mday = d->day;
|
||||
base.tm_mday += days;
|
||||
mktime(&base);
|
||||
out->year = base.tm_year + 1900;
|
||||
out->month = base.tm_mon + 1;
|
||||
out->day = base.tm_mday;
|
||||
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_today(date_t* out) {
|
||||
if (!out) return -1;
|
||||
time_t now = time(NULL);
|
||||
struct tm* t = localtime(&now);
|
||||
out->year = t->tm_year + 1900;
|
||||
out->month = t->tm_mon + 1;
|
||||
out->day = t->tm_mday;
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,109 +1,73 @@
|
|||
/*
|
||||
* 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;
|
||||
#define MSG_HEADER_SIZE 4
|
||||
#define MSG_MAX_SIZE 4096
|
||||
|
||||
if (!out || !field || len < 0)
|
||||
return;
|
||||
typedef struct {
|
||||
char type[MSG_HEADER_SIZE + 1];
|
||||
int length;
|
||||
char* payload;
|
||||
} message_t;
|
||||
|
||||
/* 우측 공백 제거하여 널 종료 문자열 생성 */
|
||||
end = len;
|
||||
while (end > 0 && (field[end - 1] == ' ' || field[end - 1] == '\0'))
|
||||
end--;
|
||||
|
||||
memcpy(out, field, end);
|
||||
out[end] = '\0';
|
||||
}
|
||||
|
||||
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_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);
|
||||
int msg_create(const char* type, const void* data, int len, message_t* out) {
|
||||
if (!type || !out) return -1;
|
||||
strncpy(out->type, type, MSG_HEADER_SIZE);
|
||||
out->type[MSG_HEADER_SIZE] = '\0';
|
||||
out->length = len;
|
||||
out->payload = malloc(len);
|
||||
if (!out->payload && len > 0) return -1;
|
||||
if (data && len > 0) memcpy(out->payload, data, 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;
|
||||
int msg_encode(const message_t* msg, char* buf, size_t len) {
|
||||
if (!msg || !buf) return -1;
|
||||
int pos = 0;
|
||||
pos += snprintf(buf + pos, len - pos, "%s|%d|", msg->type, msg->length);
|
||||
if (msg->payload && msg->length > 0) {
|
||||
memcpy(buf + pos, msg->payload, msg->length);
|
||||
pos += msg->length;
|
||||
}
|
||||
return 1;
|
||||
return pos;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
int msg_decode(const char* buf, size_t len, message_t* out) {
|
||||
if (!buf || !out) return -1;
|
||||
const char* pipe1 = strchr(buf, '|');
|
||||
if (!pipe1) return -1;
|
||||
size_t type_len = pipe1 - buf;
|
||||
if (type_len > MSG_HEADER_SIZE) type_len = MSG_HEADER_SIZE;
|
||||
strncpy(out->type, buf, type_len);
|
||||
out->type[type_len] = '\0';
|
||||
const char* pipe2 = strchr(pipe1 + 1, '|');
|
||||
if (!pipe2) return -1;
|
||||
sscanf(pipe1 + 1, "%d", &out->length);
|
||||
const char* payload_start = pipe2 + 1;
|
||||
size_t payload_len = len - (payload_start - buf);
|
||||
if (payload_len > 0 && payload_len <= MSG_MAX_SIZE) {
|
||||
out->payload = malloc(payload_len);
|
||||
if (!out->payload) return -1;
|
||||
memcpy(out->payload, payload_start, payload_len);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void msg_free(message_t* msg) {
|
||||
if (msg) {
|
||||
free(msg->payload);
|
||||
msg->payload = NULL;
|
||||
msg->length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int msg_checksum(const message_t* msg) {
|
||||
if (!msg) return 0;
|
||||
int sum = 0;
|
||||
for (int i = 0; i < msg->length; i++) {
|
||||
sum += (unsigned char)msg->payload[i];
|
||||
}
|
||||
return sum & 0xFFFF;
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue