diff --git a/src/main/java/com/eactive/eai/transformer/function/generic/ConcatN.java b/src/main/java/com/eactive/eai/transformer/function/generic/ConcatN.java new file mode 100644 index 0000000..e79f11c --- /dev/null +++ b/src/main/java/com/eactive/eai/transformer/function/generic/ConcatN.java @@ -0,0 +1,38 @@ +package com.eactive.eai.transformer.function.generic; + +import java.util.Stack; + +import org.nfunk.jep.ParseException; +import org.nfunk.jep.function.PostfixMathCommand; + +public class ConcatN extends PostfixMathCommand { + public ConcatN() { + numberOfParameters = -1; + } + + @SuppressWarnings("unchecked") + public void run(Stack inStack) throws ParseException { + checkStack(inStack); + + int n = curNumberOfParameters; + if (n < 1) { + throw new ParseException("concatN() requires at least 1 parameter"); + } + + String[] params = new String[n]; + for (int i = n - 1; i >= 0; i--) { + Object p = inStack.pop(); + if (!(p instanceof String)) { + throw new ParseException("Invalid parameter type: parameter " + (i + 1) + " is not a string"); + } + params[i] = (String) p; + } + + StringBuilder sb = new StringBuilder(); + for (String s : params) { + sb.append(s); + } + + inStack.push(sb.toString()); + } +} diff --git a/src/test/java/com/eactive/eai/transformer/ConcatNFunctionTest.java b/src/test/java/com/eactive/eai/transformer/ConcatNFunctionTest.java new file mode 100644 index 0000000..b876437 --- /dev/null +++ b/src/test/java/com/eactive/eai/transformer/ConcatNFunctionTest.java @@ -0,0 +1,164 @@ +package com.eactive.eai.transformer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.nfunk.jep.JEP; + +import com.eactive.eai.transformer.function.generic.ConcatN; + +@DisplayName("ConcatN 가변 파라미터 함수 테스트") +class ConcatNFunctionTest { + + private JEP jep; + + @BeforeEach + void setUp() { + jep = new JEP(); + jep.addStandardFunctions(); + jep.addStandardConstants(); + jep.setAllowUndeclared(true); + jep.addFunction("concatN", new ConcatN()); + } + + private String eval(String expr) throws Exception { + jep.parseExpression(expr); + assertFalse(jep.hasError(), "파싱 오류: " + jep.getErrorInfo()); + Object result = jep.getValueAsObject(); + assertFalse(jep.hasError(), "평가 오류: " + jep.getErrorInfo()); + return (String) result; + } + + // ───────────────────────────────────────────────────────────────── + // 파라미터 수 변화 테스트 + // ───────────────────────────────────────────────────────────────── + + @Test + @DisplayName("파라미터 1개 — 입력값 그대로 반환") + void testOneParam() throws Exception { + jep.addVariable("a", "Hello"); + + String result = eval("concatN(a)"); + + assertEquals("Hello", result); + } + + @Test + @DisplayName("파라미터 2개 — 기존 Concat 동작과 동일") + void testTwoParams() throws Exception { + jep.addVariable("a", "Hello"); + jep.addVariable("b", "World"); + + String result = eval("concatN(a, b)"); + + assertEquals("HelloWorld", result); + } + + @Test + @DisplayName("파라미터 3개") + void testThreeParams() throws Exception { + jep.addVariable("a", "2024"); + jep.addVariable("sep", "-"); + jep.addVariable("b", "01"); + + String result = eval("concatN(a, sep, b)"); + + assertEquals("2024-01", result); + } + + @Test + @DisplayName("파라미터 5개 — 날짜 + 시간 조합") + void testFiveParams() throws Exception { + jep.addVariable("year", "2024"); + jep.addVariable("dash", "-"); + jep.addVariable("month", "05"); + jep.addVariable("dash2", "-"); + jep.addVariable("day", "08"); + + String result = eval("concatN(year, dash, month, dash2, day)"); + + assertEquals("2024-05-08", result); + } + + @Test + @DisplayName("파라미터 7개 — 날짜 + 공백 + 시간 조합") + void testSevenParams() throws Exception { + jep.addVariable("year", "2024"); + jep.addVariable("dash", "-"); + jep.addVariable("month", "05"); + jep.addVariable("day", "08"); + jep.addVariable("space", " "); + jep.addVariable("hour", "14"); + jep.addVariable("minute", "30"); + + String result = eval("concatN(year, dash, month, dash, day, space, hour)"); + + assertEquals("2024-05-08 14", result); + } + + // ───────────────────────────────────────────────────────────────── + // 변수 재할당 후 동일 표현식 재평가 — 파라미터 수 유지, 값만 변경 + // ───────────────────────────────────────────────────────────────── + + @Test + @DisplayName("동일 표현식(3개), 변수 값만 변경하여 반복 평가") + void testReuseExpressionWithDifferentValues() throws Exception { + jep.parseExpression("concatN(code, dash, seq)"); + assertFalse(jep.hasError()); + + jep.addVariable("code", "KOR"); + jep.addVariable("dash", "-"); + jep.addVariable("seq", "001"); + assertEquals("KOR-001", (String) jep.getValueAsObject()); + + jep.addVariable("code", "USA"); + jep.addVariable("seq", "999"); + assertEquals("USA-999", (String) jep.getValueAsObject()); + + jep.addVariable("code", "JPN"); + jep.addVariable("seq", "042"); + assertEquals("JPN-042", (String) jep.getValueAsObject()); + } + + // ───────────────────────────────────────────────────────────────── + // 리터럴 / 혼합 사용 + // ───────────────────────────────────────────────────────────────── + + @Test + @DisplayName("리터럴 문자열만 5개 사용") + void testLiteralsOnly() throws Exception { + String result = eval("concatN(\"A\", \"B\", \"C\", \"D\", \"E\")"); + + assertEquals("ABCDE", result); + } + + @Test + @DisplayName("변수와 리터럴 혼합 — 괄호로 감싸기") + void testMixedVariableAndLiteral() throws Exception { + jep.addVariable("code", "KOR"); + jep.addVariable("seq", "001"); + + String result = eval("concatN(\"[\", code, \"-\", seq, \"]\")"); + + assertEquals("[KOR-001]", result); + } + + // ───────────────────────────────────────────────────────────────── + // 오류 케이스 + // ───────────────────────────────────────────────────────────────── + + @Test + @DisplayName("비문자열(숫자) 파라미터 포함 → 오류 발생") + void testNonStringParamCausesError() { + // JEP 는 정수 리터럴을 Double 로 처리하므로 평가 시 오류가 발생해야 함 + jep.parseExpression("concatN(\"A\", 123, \"C\")"); + if (!jep.hasError()) { + jep.getValueAsObject(); + assertTrue(jep.hasError(), "비문자열 파라미터가 포함된 경우 오류가 발생해야 합니다"); + } + } +} diff --git a/src/test/java/com/eactive/eai/transformer/ConcatNParserTest.java b/src/test/java/com/eactive/eai/transformer/ConcatNParserTest.java new file mode 100644 index 0000000..8a80b9f --- /dev/null +++ b/src/test/java/com/eactive/eai/transformer/ConcatNParserTest.java @@ -0,0 +1,152 @@ +package com.eactive.eai.transformer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.eactive.eai.transformer.engine.Parser; +import com.eactive.eai.transformer.function.generic.ConcatN; + +/** + * ParserTest 스타일로 Parser 를 통해 concatN 함수를 검증한다. + * + * - 표현식은 parse() 한 번 → 변수값만 교체하며 반복 평가 (JEP 재사용 패턴) + * - 변수 : parser.addVariable() 로 바인딩 + * - 상수 : 표현식 안에 "문자열" 리터럴로 직접 기입 + */ +@DisplayName("ConcatN - Parser 변수/상수 혼합 테스트") +class ConcatNParserTest { + + private Parser parser; + + @BeforeEach + void setUp() { + parser = new Parser(); + // DB 등록 전 테스트이므로 직접 등록 + parser.getJep().addFunction("concatn", new ConcatN()); + } + + // ───────────────────────────────────────────────────────────────── + // 1. 변수만 사용 + // ───────────────────────────────────────────────────────────────── + + @Test + @DisplayName("변수 2개 — parse 1회, 값 교체 반복 평가") + void testTwoVariables() throws Exception { + parser.parse("concatn(a, b)"); + + parser.addVariable("a", "Hello"); + parser.addVariable("b", "World"); + assertEquals("HelloWorld", parser.getValueAsObject()); + + parser.addVariable("a", "foo"); + parser.addVariable("b", "bar"); + assertEquals("foobar", parser.getValueAsObject()); + } + + @Test + @DisplayName("변수 3개") + void testThreeVariables() throws Exception { + parser.parse("concatn(v1, v2, v3)"); + + parser.addVariable("v1", "AAA"); + parser.addVariable("v2", "BBB"); + parser.addVariable("v3", "CCC"); + assertEquals("AAABBBCCC", parser.getValueAsObject()); + } + + @Test + @DisplayName("변수 5개") + void testFiveVariables() throws Exception { + parser.parse("concatn(y, m, d, h, mi)"); + + parser.addVariable("y", "2024"); + parser.addVariable("m", "05"); + parser.addVariable("d", "08"); + parser.addVariable("h", "14"); + parser.addVariable("mi", "30"); + assertEquals("20240508" + "14" + "30", parser.getValueAsObject()); + } + + // ───────────────────────────────────────────────────────────────── + // 2. 변수 + 상수(리터럴) 혼합 + // ───────────────────────────────────────────────────────────────── + + @Test + @DisplayName("날짜 구분자 '-' 상수 삽입 — 변수 교체 반복") + void testDateWithSeparator() throws Exception { + parser.parse("concatn(year, \"-\", month, \"-\", day)"); + + parser.addVariable("year", "2024"); + parser.addVariable("month", "01"); + parser.addVariable("day", "15"); + assertEquals("2024-01-15", parser.getValueAsObject()); + + parser.addVariable("year", "2025"); + parser.addVariable("month", "12"); + parser.addVariable("day", "31"); + assertEquals("2025-12-31", parser.getValueAsObject()); + } + + @Test + @DisplayName("접두·접미 상수로 감싸기") + void testWrapWithConstants() throws Exception { + parser.parse("concatn(\"[\", code, \"-\", seq, \"]\")"); + + parser.addVariable("code", "KOR"); + parser.addVariable("seq", "001"); + assertEquals("[KOR-001]", parser.getValueAsObject()); + + parser.addVariable("code", "USA"); + parser.addVariable("seq", "999"); + assertEquals("[USA-999]", parser.getValueAsObject()); + } + + @Test + @DisplayName("앞에 상수 접두어 + 변수 1개") + void testConstantPrefix() throws Exception { + parser.parse("concatn(\"REQ-\", txId)"); + + parser.addVariable("txId", "20240508001"); + assertEquals("REQ-20240508001", parser.getValueAsObject()); + + parser.addVariable("txId", "20240508002"); + assertEquals("REQ-20240508002", parser.getValueAsObject()); + } + + @Test + @DisplayName("변수 사이에 공백 상수 삽입") + void testSpaceSeparator() throws Exception { + parser.parse("concatn(firstName, \" \", lastName)"); + + parser.addVariable("firstName", "Gil-dong"); + parser.addVariable("lastName", "Hong"); + assertEquals("Gil-dong Hong", parser.getValueAsObject()); + } + + // ───────────────────────────────────────────────────────────────── + // 3. 성능 측정 (ParserTest 스타일) + // ───────────────────────────────────────────────────────────────── + + @Test + @DisplayName("100만 회 반복 평가 — parse 1회 재사용 성능") + void testPerformance() throws Exception { + String expr = "concatn(code, \"-\", seq)"; + parser.parse(expr); + + Object result = null; + long t = System.currentTimeMillis(); + for (int i = 0; i < 1_000_000; i++) { + parser.addVariable("code", "KOR"); + parser.addVariable("seq", String.valueOf(i)); + result = parser.getValueAsObject(); + } + long elapsed = System.currentTimeMillis() - t; + + System.out.println(String.format("%s = %s %dms", expr, result, elapsed)); + + assertEquals("KOR-999999", result); + } +}