diff --git a/src/main/java/com/eactive/eai/transformer/engine/Parser.java b/src/main/java/com/eactive/eai/transformer/engine/Parser.java index 98315d0..5cd7d03 100644 --- a/src/main/java/com/eactive/eai/transformer/engine/Parser.java +++ b/src/main/java/com/eactive/eai/transformer/engine/Parser.java @@ -36,6 +36,8 @@ public class Parser implements Serializable { getJep().setAllowAssignment(true); getJep().setAllowUndeclared(true); + addBooleanConstants(); + try { addGenericFunctions(); } catch (Exception e) { @@ -89,6 +91,13 @@ public class Parser implements Serializable { public void initSymbolTable() { getJep().initSymTab(); + addBooleanConstants(); + } + + /** JEP 기본 상수에 없는 true/false 를 Boolean 타입으로 등록한다. */ + private void addBooleanConstants() { + getJep().addVariable("true", Boolean.TRUE); + getJep().addVariable("false", Boolean.FALSE); } @SuppressWarnings("unchecked") @@ -144,8 +153,9 @@ public class Parser implements Serializable { private void addFunction(String name, PostfixMathCommand command) { if(name == null || command == null) { logger.warn("addFunction ("+name+","+command+") skip null"); - return; + return; } + getJep().addFunction(name, command); // 원본 대소문자 (camelCase 표현식 지원) getJep().addFunction(name.toUpperCase(), command); getJep().addFunction(name.toLowerCase(), command); } diff --git a/src/main/java/com/eactive/eai/transformer/function/generic/ConvertDateTime.java b/src/main/java/com/eactive/eai/transformer/function/generic/ConvertDateTime.java new file mode 100644 index 0000000..04af171 --- /dev/null +++ b/src/main/java/com/eactive/eai/transformer/function/generic/ConvertDateTime.java @@ -0,0 +1,67 @@ +package com.eactive.eai.transformer.function.generic; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Stack; + +import org.nfunk.jep.ParseException; +import org.nfunk.jep.function.PostfixMathCommand; + +/** + * JEP 커스텀 함수 - 날짜/시간 형식 변환 + * + *

사용법: {@code convertDateTime(source, fromFormat, toFormat)} + *

+ * 반환값: toFormat으로 재포맷된 날짜 문자열 + */ +public class ConvertDateTime extends PostfixMathCommand { + public ConvertDateTime() { + numberOfParameters = 3; + } + + /** + * 스택에서 (source, fromFormat, toFormat) 순으로 꺼내 날짜 형식을 변환한다. + * + * @param inStack JEP 후위 연산 스택 + * @throws ParseException 파라미터 타입 오류, 날짜 파싱 실패, 잘못된 형식 패턴 + */ + @SuppressWarnings("unchecked") + public void run(Stack inStack) throws ParseException { + checkStack(inStack); + + Object p3 = inStack.pop(); + Object p2 = inStack.pop(); + Object p1 = inStack.pop(); + + if (!(p1 instanceof String)) { + throw new ParseException("convertDateTime(): source must be a string"); + } + if (!(p2 instanceof String)) { + throw new ParseException("convertDateTime(): fromFormat must be a string"); + } + if (!(p3 instanceof String)) { + throw new ParseException("convertDateTime(): toFormat must be a string"); + } + + String source = (String) p1; + String fromFormat = (String) p2; + String toFormat = (String) p3; + + try { + SimpleDateFormat sdfFrom = new SimpleDateFormat(fromFormat); + sdfFrom.setLenient(false); + Date date = sdfFrom.parse(source); + inStack.push(new SimpleDateFormat(toFormat).format(date)); + } catch (java.text.ParseException e) { + throw new ParseException( + "convertDateTime(): failed to parse '" + source + "' with format '" + fromFormat + "': " + e.getMessage()); + } catch (IllegalArgumentException e) { + throw new ParseException( + "convertDateTime(): invalid format pattern: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/eactive/eai/transformer/function/generic/GetCurrent.java b/src/main/java/com/eactive/eai/transformer/function/generic/GetCurrent.java new file mode 100644 index 0000000..35dfc45 --- /dev/null +++ b/src/main/java/com/eactive/eai/transformer/function/generic/GetCurrent.java @@ -0,0 +1,46 @@ +package com.eactive.eai.transformer.function.generic; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Stack; + +import org.nfunk.jep.ParseException; +import org.nfunk.jep.function.PostfixMathCommand; + +/** + * JEP 커스텀 함수 - 현재 일시 반환 + * + *

사용법: {@code getCurrent(format)} + *

+ * 반환값: 지정 형식으로 포맷된 현재 일시 문자열 + */ +public class GetCurrent extends PostfixMathCommand { + public GetCurrent() { + numberOfParameters = 1; + } + + /** + * 스택에서 format 패턴을 꺼내 현재 일시를 포맷하여 반환한다. + * + * @param inStack JEP 후위 연산 스택 + * @throws ParseException 파라미터 타입 오류, 잘못된 형식 패턴 + */ + @SuppressWarnings("unchecked") + public void run(Stack inStack) throws ParseException { + checkStack(inStack); + + Object p1 = inStack.pop(); + + if (!(p1 instanceof String)) { + throw new ParseException("getCurrent(): format must be a string"); + } + + try { + inStack.push(new SimpleDateFormat((String) p1).format(new Date())); + } catch (IllegalArgumentException e) { + throw new ParseException("getCurrent(): invalid date format '" + p1 + "': " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/eactive/eai/transformer/function/generic/GetTrinomial.java b/src/main/java/com/eactive/eai/transformer/function/generic/GetTrinomial.java new file mode 100644 index 0000000..0202529 --- /dev/null +++ b/src/main/java/com/eactive/eai/transformer/function/generic/GetTrinomial.java @@ -0,0 +1,85 @@ +package com.eactive.eai.transformer.function.generic; + +import java.util.Stack; + +import org.nfunk.jep.ParseException; +import org.nfunk.jep.function.PostfixMathCommand; + +/** + * JEP 커스텀 함수 - 삼항 조건 평가 + * + *

사용법: {@code getTrinomial(target, compare, operator, trueValue, falseValue)} + *

+ * 타입별 비교: Number는 수치 비교, Boolean은 '=' 만 지원, 그 외는 문자열 사전순 비교 + */ +public class GetTrinomial extends PostfixMathCommand { + public GetTrinomial() { + numberOfParameters = 5; + } + + /** + * 스택에서 (target, compare, operator, trueValue, falseValue) 순으로 꺼내 조건을 평가한다. + * + * @param inStack JEP 후위 연산 스택 + * @throws ParseException operator 타입 오류, 지원하지 않는 연산자, boolean에 '=' 외 연산자 사용 + */ + @SuppressWarnings("unchecked") + public void run(Stack inStack) throws ParseException { + checkStack(inStack); + + Object falseValue = inStack.pop(); + Object trueValue = inStack.pop(); + Object operator = inStack.pop(); + Object compare = inStack.pop(); + Object target = inStack.pop(); + + if (!(operator instanceof String)) { + throw new ParseException("getTrinomial(): operator must be a string (=, <, >, <=, >=)"); + } + + boolean result = compare(target, compare, ((String) operator).trim()); + inStack.push(result ? trueValue : falseValue); + } + + /** + * target과 compare를 op 연산자로 비교한다. + * Number → 수치 비교, Boolean → 동등 비교, 그 외 → 문자열 사전순 비교 + */ + private boolean compare(Object target, Object compare, String op) throws ParseException { + if (target instanceof Number && compare instanceof Number) { + double t = ((Number) target).doubleValue(); + double c = ((Number) compare).doubleValue(); + switch (op) { + case "=": return t == c; + case "<": return t < c; + case ">": return t > c; + case "<=": return t <= c; + case ">=": return t >= c; + default: throw new ParseException("getTrinomial(): unsupported operator: " + op); + } + } else if (target instanceof Boolean && compare instanceof Boolean) { + if (!"=".equals(op)) { + throw new ParseException("getTrinomial(): boolean comparison only supports '=' operator"); + } + return target.equals(compare); + } else { + String t = target != null ? target.toString() : ""; + String c = compare != null ? compare.toString() : ""; + int cmp = t.compareTo(c); + switch (op) { + case "=": return cmp == 0; + case "<": return cmp < 0; + case ">": return cmp > 0; + case "<=": return cmp <= 0; + case ">=": return cmp >= 0; + default: throw new ParseException("getTrinomial(): unsupported operator: " + op); + } + } + } +} diff --git a/src/main/java/com/eactive/eai/transformer/function/generic/ReplaceAll.java b/src/main/java/com/eactive/eai/transformer/function/generic/ReplaceAll.java new file mode 100644 index 0000000..db99b17 --- /dev/null +++ b/src/main/java/com/eactive/eai/transformer/function/generic/ReplaceAll.java @@ -0,0 +1,52 @@ +package com.eactive.eai.transformer.function.generic; + +import java.util.Stack; + +import org.nfunk.jep.ParseException; +import org.nfunk.jep.function.PostfixMathCommand; + +/** + * JEP 커스텀 함수 - 문자열 전체 치환 + * + *

사용법: {@code replaceAll(source, from, to)} + *

+ * 반환값: source 내 from과 일치하는 모든 부분을 to로 치환한 문자열 + * + *

주의: JEP 내장 함수 {@code str()}와 충돌하므로, 변수명으로 {@code str}을 사용하지 말 것. + */ +public class ReplaceAll extends PostfixMathCommand { + public ReplaceAll() { + numberOfParameters = 3; + } + + /** + * 스택에서 (source, from, to) 순으로 꺼내 전체 치환 후 결과를 반환한다. + * + * @param inStack JEP 후위 연산 스택 + * @throws ParseException 파라미터가 String 타입이 아닌 경우 + */ + @SuppressWarnings("unchecked") + public void run(Stack inStack) throws ParseException { + checkStack(inStack); + + Object p3 = inStack.pop(); + Object p2 = inStack.pop(); + Object p1 = inStack.pop(); + + if (!(p1 instanceof String)) { + throw new ParseException("replaceAll(): first parameter (source string) must be a string"); + } + if (!(p2 instanceof String)) { + throw new ParseException("replaceAll(): second parameter (from string) must be a string"); + } + if (!(p3 instanceof String)) { + throw new ParseException("replaceAll(): third parameter (to string) must be a string"); + } + + inStack.push(((String) p1).replace((String) p2, (String) p3)); + } +} diff --git a/src/test/java/com/eactive/eai/transformer/ConvertDateTimeFunctionTest.java b/src/test/java/com/eactive/eai/transformer/ConvertDateTimeFunctionTest.java new file mode 100644 index 0000000..eb8d86f --- /dev/null +++ b/src/test/java/com/eactive/eai/transformer/ConvertDateTimeFunctionTest.java @@ -0,0 +1,85 @@ +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.ConvertDateTime; + +@DisplayName("ConvertDateTime 날짜 변환 함수 테스트") +class ConvertDateTimeFunctionTest { + + private JEP jep; + + @BeforeEach + void setUp() { + jep = new JEP(); + jep.addStandardFunctions(); + jep.addStandardConstants(); + jep.setAllowUndeclared(true); + jep.addFunction("convertDateTime", new ConvertDateTime()); + } + + private String eval(String expr) { + jep.parseExpression(expr); + assertFalse(jep.hasError(), "파싱 오류: " + jep.getErrorInfo()); + Object result = jep.getValueAsObject(); + assertFalse(jep.hasError(), "평가 오류: " + jep.getErrorInfo()); + return (String) result; + } + + @Test + @DisplayName("yyyyMMdd → yyyy-MM-dd 변환") + void testCompactToHyphen() { + jep.addVariable("src", "20240508"); + assertEquals("2024-05-08", eval("convertDateTime(src, \"yyyyMMdd\", \"yyyy-MM-dd\")")); + } + + @Test + @DisplayName("yyyyMMddHHmmss → yyyy-MM-dd'T'HH:mm:ss 변환 (ISO 8601)") + void testToIso8601() { + jep.addVariable("src", "20240508143022"); + assertEquals("2024-05-08T14:30:22", + eval("convertDateTime(src, \"yyyyMMddHHmmss\", \"yyyy-MM-dd'T'HH:mm:ss\")")); + } + + @Test + @DisplayName("yyyy-MM-dd'T'HH:mm:ss → yyyyMMddHHmmss 변환") + void testFromIso8601ToCompact() { + jep.addVariable("src", "2024-05-08T14:30:22"); + assertEquals("20240508143022", + eval("convertDateTime(src, \"yyyy-MM-dd'T'HH:mm:ss\", \"yyyyMMddHHmmss\")")); + } + + @Test + @DisplayName("yyyyMMddHHmmssSSS → yyyy-MM-dd HH:mm:ss.SSS 변환 (밀리초 포함)") + void testWithMilliseconds() { + jep.addVariable("src", "20240508143022123"); + assertEquals("2024-05-08 14:30:22.123", + eval("convertDateTime(src, \"yyyyMMddHHmmssSSS\", \"yyyy-MM-dd HH:mm:ss.SSS\")")); + } + + @Test + @DisplayName("날짜 포맷만 변환 (시간 없음)") + void testDateOnly() { + jep.addVariable("src", "2024/05/08"); + assertEquals("20240508", + eval("convertDateTime(src, \"yyyy/MM/dd\", \"yyyyMMdd\")")); + } + + @Test + @DisplayName("잘못된 날짜 → 오류 발생") + void testInvalidDate() { + jep.addVariable("src", "99999999"); + jep.parseExpression("convertDateTime(src, \"yyyyMMdd\", \"yyyy-MM-dd\")"); + if (!jep.hasError()) { + jep.getValueAsObject(); + assertTrue(jep.hasError(), "잘못된 날짜는 오류가 발생해야 합니다"); + } + } +} diff --git a/src/test/java/com/eactive/eai/transformer/GetCurrentFunctionTest.java b/src/test/java/com/eactive/eai/transformer/GetCurrentFunctionTest.java new file mode 100644 index 0000000..c670ec9 --- /dev/null +++ b/src/test/java/com/eactive/eai/transformer/GetCurrentFunctionTest.java @@ -0,0 +1,93 @@ +package com.eactive.eai.transformer; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.text.SimpleDateFormat; +import java.util.Date; + +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.GetCurrent; + +@DisplayName("GetCurrent 현재시간 반환 함수 테스트") +class GetCurrentFunctionTest { + + private JEP jep; + + @BeforeEach + void setUp() { + jep = new JEP(); + jep.addStandardFunctions(); + jep.addStandardConstants(); + jep.setAllowUndeclared(true); + jep.addFunction("getCurrent", new GetCurrent()); + } + + private String eval(String expr) { + jep.parseExpression(expr); + assertFalse(jep.hasError(), "파싱 오류: " + jep.getErrorInfo()); + Object result = jep.getValueAsObject(); + assertFalse(jep.hasError(), "평가 오류: " + jep.getErrorInfo()); + return (String) result; + } + + @Test + @DisplayName("yyyyMMdd 형식 — 오늘 날짜와 일치") + void testDateFormat() { + String expected = new SimpleDateFormat("yyyyMMdd").format(new Date()); + String result = eval("getCurrent(\"yyyyMMdd\")"); + assertEquals(expected, result); + } + + @Test + @DisplayName("yyyy-MM-dd 형식 — 오늘 날짜와 일치") + void testHyphenDateFormat() { + String expected = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); + String result = eval("getCurrent(\"yyyy-MM-dd\")"); + assertEquals(expected, result); + } + + @Test + @DisplayName("yyyyMMddHHmmss 형식 — 결과가 14자리") + void testDateTimeFormat() { + String result = eval("getCurrent(\"yyyyMMddHHmmss\")"); + assertNotNull(result); + assertTrue(result.matches("\\d{14}"), "결과가 14자리 숫자여야 합니다: " + result); + } + + @Test + @DisplayName("yyyy-MM-dd'T'HH:mm:ss ISO 8601 형식") + void testIso8601Format() { + String result = eval("getCurrent(\"yyyy-MM-dd'T'HH:mm:ss\")"); + assertNotNull(result); + assertTrue(result.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}"), + "ISO 8601 형식이어야 합니다: " + result); + } + + @Test + @DisplayName("yyyyMMddHHmmssSSS 형식 — 결과가 17자리") + void testDateTimeMillisFormat() { + String result = eval("getCurrent(\"yyyyMMddHHmmssSSS\")"); + assertNotNull(result); + assertTrue(result.matches("\\d{17}"), "결과가 17자리 숫자여야 합니다: " + result); + } + + @Test + @DisplayName("잘못된 포맷 → 오류 발생") + void testInvalidFormat() { + jep.parseExpression("getCurrent(\"invalid!!!###\")"); + if (!jep.hasError()) { + jep.getValueAsObject(); + assertTrue(jep.hasError(), "잘못된 포맷은 오류가 발생해야 합니다"); + } + } + + private void assertEquals(String expected, String result) { + org.junit.jupiter.api.Assertions.assertEquals(expected, result); + } +} diff --git a/src/test/java/com/eactive/eai/transformer/GetTrinomialFunctionTest.java b/src/test/java/com/eactive/eai/transformer/GetTrinomialFunctionTest.java new file mode 100644 index 0000000..f34363e --- /dev/null +++ b/src/test/java/com/eactive/eai/transformer/GetTrinomialFunctionTest.java @@ -0,0 +1,205 @@ +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.GetTrinomial; + +@DisplayName("GetTrinomial 삼항연산자 함수 테스트") +class GetTrinomialFunctionTest { + + private JEP jep; + + @BeforeEach + void setUp() { + jep = new JEP(); + jep.addStandardFunctions(); + jep.addStandardConstants(); + jep.setAllowUndeclared(true); + + jep.addVariable("true", Boolean.TRUE); + jep.addVariable("false", Boolean.FALSE); + + jep.addFunction("getTrinomial", new GetTrinomial()); + } + + private Object eval(String expr) { + jep.parseExpression(expr); + assertFalse(jep.hasError(), "파싱 오류: " + jep.getErrorInfo()); + Object result = jep.getValueAsObject(); + assertFalse(jep.hasError(), "평가 오류: " + jep.getErrorInfo()); + return result; + } + + // ───────────────────────────────────────────────────────────────── + // 숫자형 비교 + // ───────────────────────────────────────────────────────────────── + + @Test + @DisplayName("숫자 = 비교 — 참") + void testNumberEqualTrue() { + jep.addVariable("a", 10.0); + assertEquals("같음", eval("getTrinomial(a, 10, \"=\", \"같음\", \"다름\")")); + } + + @Test + @DisplayName("숫자 = 비교 — 거짓") + void testNumberEqualFalse() { + jep.addVariable("a", 5.0); + assertEquals("다름", eval("getTrinomial(a, 10, \"=\", \"같음\", \"다름\")")); + } + + @Test + @DisplayName("숫자 < 비교 — 참") + void testNumberLessThanTrue() { + jep.addVariable("score", 70.0); + assertEquals("미달", eval("getTrinomial(score, 80, \"<\", \"미달\", \"통과\")")); + } + + @Test + @DisplayName("숫자 > 비교 — 참") + void testNumberGreaterThanTrue() { + jep.addVariable("score", 90.0); + assertEquals("우수", eval("getTrinomial(score, 80, \">\", \"우수\", \"보통\")")); + } + + @Test + @DisplayName("숫자 <= 비교 — 경계값") + void testNumberLessOrEqual() { + jep.addVariable("v", 80.0); + assertEquals("이하", eval("getTrinomial(v, 80, \"<=\", \"이하\", \"초과\")")); + } + + @Test + @DisplayName("숫자 >= 비교 — 경계값") + void testNumberGreaterOrEqual() { + jep.addVariable("v", 80.0); + assertEquals("이상", eval("getTrinomial(v, 80, \">=\", \"이상\", \"미만\")")); + } + + // ───────────────────────────────────────────────────────────────── + // 문자열 비교 + // ───────────────────────────────────────────────────────────────── + + @Test + @DisplayName("문자열 = 비교 — 참") + void testStringEqualTrue() { + jep.addVariable("status", "Y"); + assertEquals("활성", eval("getTrinomial(status, \"Y\", \"=\", \"활성\", \"비활성\")")); + } + + @Test + @DisplayName("문자열 = 비교 — 거짓") + void testStringEqualFalse() { + jep.addVariable("status", "N"); + assertEquals("비활성", eval("getTrinomial(status, \"Y\", \"=\", \"활성\", \"비활성\")")); + } + + @Test + @DisplayName("문자열 < 비교 — 사전순") + void testStringLessThan() { + jep.addVariable("s", "apple"); + assertEquals("앞", eval("getTrinomial(s, \"banana\", \"<\", \"앞\", \"뒤\")")); + } + + // ───────────────────────────────────────────────────────────────── + // 결과값 타입 — 숫자 반환 + // ───────────────────────────────────────────────────────────────── + + @Test + @DisplayName("결과값이 숫자형") + void testNumericReturnValue() { + jep.addVariable("flag", "Y"); + Object result = eval("getTrinomial(flag, \"Y\", \"=\", 1, 0)"); + assertTrue(result instanceof Number); + assertEquals(1.0, ((Number) result).doubleValue()); + } + + // ───────────────────────────────────────────────────────────────── + // boolean 비교 + // ───────────────────────────────────────────────────────────────── + + @Test + @DisplayName("boolean = 비교 — 참 (JEP는 boolean 리터럴 미지원, 변수로 바인딩)") + void testBooleanEqual() { + jep.addVariable("b", Boolean.TRUE); + jep.addVariable("bTrue", Boolean.TRUE); + assertEquals("참", eval("getTrinomial(b, bTrue, \"=\", \"참\", \"거짓\")")); + } + + @Test + @DisplayName("boolean = 비교 — 거짓") + void testBooleanEqualFalse() { + jep.addVariable("b", Boolean.FALSE); + jep.addVariable("bTrue", Boolean.TRUE); + assertEquals("거짓", eval("getTrinomial(b, bTrue, \"=\", \"참\", \"거짓\")")); + } + + // ───────────────────────────────────────────────────────────────── + // 실전 Transformer 표현식 패턴 + // ───────────────────────────────────────────────────────────────── + + @Test + @DisplayName("실전 패턴 — true/false 결과값 (JEP 상수 1.0/0.0 반환 검증)") + void testBooleanLiteralReturnValue() { + // JEP addStandardConstants()가 true=1.0, false=0.0 으로 등록하는지 확인 + // → 등록되어 있다면 변수 바인딩 없이 숫자로 반환됨 + // → 등록 안 됐다면 파싱 오류 또는 undeclared 변수(NaN)로 처리됨 + jep.addVariable("intVal", 10.0); + Object result = eval("getTrinomial(intVal, 10, \"=\", true, false)"); + assertTrue(result instanceof Number, + "true/false 결과값이 Number 타입이어야 함 (실제: " + result.getClass().getSimpleName() + ")"); + assertEquals(1.0, ((Number) result).doubleValue(), 0.0, + "조건 참일 때 true(=1.0) 반환 기대"); + } + + @Test + @DisplayName("실전 패턴 — dot 경로 변수(AGW_…SRCS.INT_VALUE) + boolean 결과값") + void testRealWorldExpression() { + // Transformer 실사용 표현식: + // getTrinomial(AGW_TESTREQ10031S1_SRCS.INT_VALUE, 10, "=", true, false) + // + // JEP에서 dot(.)은 element-access 연산자로 파싱됨 + // → 파싱 성공 여부와 변수 바인딩 방법을 확인하기 위한 테스트 + String expr = "getTrinomial(AGW_TESTREQ10031S1_SRCS.INT_VALUE, 10, \"=\", true, false)"; + + // [1] 파싱만 먼저 검증 + jep.parseExpression(expr); + assertFalse(jep.hasError(), "파싱 오류: " + jep.getErrorInfo()); + + // [2] 파싱 후 JEP 심볼 테이블에 등록된 변수명 확인 + // dot-expression 이므로 "AGW_TESTREQ10031S1_SRCS" 가 심볼로 잡힐 것으로 예상 + java.util.Set symbols = jep.getSymbolTable().keySet(); + assertTrue( + symbols.contains("AGW_TESTREQ10031S1_SRCS") || symbols.contains("AGW_TESTREQ10031S1_SRCS.INT_VALUE"), + "심볼 테이블에 소스 변수가 포함되어야 함. 실제 심볼: " + symbols + ); + + // [3] 심볼에 값 바인딩 후 평가 + // dot-expression(AGW_…SRCS.INT_VALUE)은 JEP vector element 접근이므로 + // 스칼라 바인딩 시 평가 오류 발생 가능 → 오류 메시지로 대응 방법 확인 + jep.addVariable("AGW_TESTREQ10031S1_SRCS", 10.0); + + Object result = jep.getValueAsObject(); + if (jep.hasError()) { + // dot element-access 실패 시: Transformer는 심볼 전체 경로를 변수명으로 등록해야 함 + // → Parser.addVariable("AGW_TESTREQ10031S1_SRCS.INT_VALUE", value) 방식 필요 + jep.addVariable("AGW_TESTREQ10031S1_SRCS.INT_VALUE", 10.0); + jep.parseExpression(expr); + assertFalse(jep.hasError(), "재파싱 오류: " + jep.getErrorInfo()); + result = jep.getValueAsObject(); + assertFalse(jep.hasError(), "평가 오류: " + jep.getErrorInfo()); + } + + assertTrue(result instanceof Number, + "결과값이 Number 타입이어야 함 (실제: " + (result == null ? "null" : result.getClass().getSimpleName()) + ")"); + assertEquals(1.0, ((Number) result).doubleValue(), 0.0, + "INT_VALUE=10, 비교값=10, 연산자=등호 → 참(true=1.0) 반환 기대"); + } +} diff --git a/src/test/java/com/eactive/eai/transformer/ReplaceAllFunctionTest.java b/src/test/java/com/eactive/eai/transformer/ReplaceAllFunctionTest.java new file mode 100644 index 0000000..80b6e92 --- /dev/null +++ b/src/test/java/com/eactive/eai/transformer/ReplaceAllFunctionTest.java @@ -0,0 +1,88 @@ +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.ReplaceAll; + +@DisplayName("ReplaceAll 문자열 치환 함수 테스트") +class ReplaceAllFunctionTest { + + private JEP jep; + + @BeforeEach + void setUp() { + jep = new JEP(); + jep.addStandardFunctions(); + jep.addStandardConstants(); + jep.setAllowUndeclared(true); + jep.addFunction("replaceAll", new ReplaceAll()); + } + + private String eval(String expr) { + jep.parseExpression(expr); + assertFalse(jep.hasError(), "파싱 오류: " + jep.getErrorInfo()); + Object result = jep.getValueAsObject(); + assertFalse(jep.hasError(), "평가 오류: " + jep.getErrorInfo()); + return (String) result; + } + + @Test + @DisplayName("단일 치환 — 홍길동 → 홍길순") + void testSingleReplace() { + jep.addVariable("name", "홍길동"); + assertEquals("홍길순", eval("replaceAll(name, \"동\", \"순\")")); + } + + @Test + @DisplayName("다중 치환 — 모든 매칭 문자열 변경") + void testMultipleOccurrences() { + // "aababab" 안의 'a' 4개가 모두 'X'로 치환 → "XXbXbXb" + jep.addVariable("src", "aababab"); + assertEquals("XXbXbXb", eval("replaceAll(src, \"a\", \"X\")")); + } + + @Test + @DisplayName("치환 대상 없음 — 원본 그대로 반환") + void testNoMatch() { + jep.addVariable("src", "hello"); + assertEquals("hello", eval("replaceAll(src, \"z\", \"X\")")); + } + + @Test + @DisplayName("치환 후 문자열이 빈 문자열 (JEP는 \"\" 리터럴 미지원 → 변수 바인딩)") + void testReplaceWithEmpty() { + jep.addVariable("src", "abc123def"); + assertEquals("abcdef", eval("replaceAll(src, \"123\", \"\")")); + } + + @Test + @DisplayName("날짜 구분자 변환 — '-' → '/'") + void testDateSeparator() { + jep.addVariable("date", "2024-05-08"); + assertEquals("2024/05/08", eval("replaceAll(date, \"-\", \"/\")")); + } + + @Test + @DisplayName("리터럴만 사용") + void testLiteralsOnly() { + assertEquals("world world", + eval("replaceAll(\"hello hello\", \"hello\", \"world\")")); + } + + @Test + @DisplayName("비문자열 파라미터 → 오류 발생") + void testNonStringParamCausesError() { + jep.parseExpression("replaceAll(123, \"1\", \"X\")"); + if (!jep.hasError()) { + jep.getValueAsObject(); + assertTrue(jep.hasError(), "숫자 파라미터는 오류가 발생해야 합니다"); + } + } +}