Skip to content

Commit

Permalink
Merge pull request #24 from Nuix/feature/primitive-type-parser
Browse files Browse the repository at this point in the history
Feature/primitive type parser
  • Loading branch information
JuicyDragon authored Aug 19, 2024
2 parents f8f158a + d02aa52 commit 3790d19
Show file tree
Hide file tree
Showing 3 changed files with 336 additions and 0 deletions.
1 change: 1 addition & 0 deletions Java/SuperUtilities/.idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package com.nuix.superutilities.misc;
import com.google.common.collect.ImmutableList;
import lombok.Getter;
import org.apache.commons.lang3.tuple.Pair;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Duration;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Getter
public class PrimitiveTypeParser {
public static final Function<String, Object> jodaTimeAutomaticParsing = new Function<>() {
private final List<Pair<Pattern, DateTimeFormatter>> formats = List.of(
// 2017-12-11T15:51:24Z
// 2022-04-20T17:12:37Z
Pair.of(
Pattern.compile("^\\d{4}-[01][0-9]-[0-3][0-9][tT][0-2][0-9]:[0-5][0-9]:\\d{2}Z$"),
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(DateTimeZone.UTC)
),

// 2017-12-11T15:51:24.000-07:00
Pair.of(
Pattern.compile("^\\d{4}-[01][0-9]-[0-3][0-9][tT][0-2][0-9]:[0-5][0-9]:\\d{2}\\.\\d{3}[+\\-]\\d{2}:\\d{2}$"),
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZZ").withZone(DateTimeZone.UTC)
),

// 6/26/2024 1:50:33 PM
Pair.of(
Pattern.compile("\\d{1,2}/\\d{1,2}/\\d{4}\\s{2}\\d{1,2}:\\d{2}:\\d{2} [AP]M"),
DateTimeFormat.forPattern("M/d/yyyy h:m:s a").withZone(DateTimeZone.UTC)
),

// 26 Jun 2024 13:50:33
Pair.of(
Pattern.compile("\\d{1,2}\\s{2}[A-Z][a-z]{2} \\d{4} \\d{2}:\\d{2}:\\d{2}"),
DateTimeFormat.forPattern("dd MMM yyyy HH:mm:ss").withZone(DateTimeZone.UTC)
)
);

@Override
public Object apply(String s) {
for (Pair<Pattern, DateTimeFormatter> format : formats) {
Matcher matcher = format.getLeft().matcher(s);
if (matcher.matches()) {
return format.getRight().parseDateTime(s);
}
}
return null;
}
};

public static final Function<String, Object> durationAutomaticParsing = new Function<>() {
private final List<Pattern> formatValidators = List.of(
Pattern.compile("(?<hours>[0-9]{2}):(?<minutes>[0-9]{2}):(?<seconds>[0-9]{2})")
);

@Override
public Object apply(String s) {
for (Pattern pattern : formatValidators) {
Matcher m = pattern.matcher(s);
if (m.matches()) {
long hours = Long.parseLong(m.group("hours"));
long minutes = Long.parseLong(m.group("minutes"));
long seconds = Long.parseLong(m.group("seconds"));
long millis = ((hours * 3600) + (minutes * 60) + seconds) * 1000;
return new Duration(millis);
}
}
return null;
}
};

public static final Function<String, Object> numericParser = new Function<>() {
private final Pattern formatValidator = Pattern.compile("^[+\\-]?[0-9]+$");

@Override
public Object apply(String s) {
if (!formatValidator.matcher(s.trim()).matches()) {
return null;
} else {
return Long.parseLong(s.trim());
}
}
};

public static final Function<String, Object> decimalParser = new Function<>() {
private final Pattern formatValidator = Pattern.compile("^[+\\-]?[0-9]+\\.[0-9]*$");

@Override
public Object apply(String s) {
if (!formatValidator.matcher(s.trim()).matches()) {
return null;
} else {
return Double.parseDouble(s.trim());
}
}
};

public static final Function<String, Object> booleanParser = new Function<>() {
private final Pattern formatValidator = Pattern.compile("^(true)|(false)$");

@Override
public Object apply(String s) {
if (!formatValidator.matcher(s.trim().toLowerCase()).matches()) {
return null;
} else {
return Boolean.parseBoolean(s.trim());
}
}
};

public static final Function<String, Object> yesNoBooleanParser = new Function<>() {
private final Pattern formatValidator = Pattern.compile("^(yes)|(no)$", Pattern.CASE_INSENSITIVE);

@Override
public Object apply(String s) {
if (!formatValidator.matcher(s.trim().toLowerCase()).matches()) {
return null;
} else {
return Boolean.parseBoolean(s.trim());
}
}
};

@Getter(lazy = true)
private static final PrimitiveTypeParser standard = buildImmutableStandard();

private static PrimitiveTypeParser buildImmutableStandard() {
PrimitiveTypeParser result = buildStandardCopy();
result.typeParsers = ImmutableList.copyOf(result.typeParsers);
return result;
}

public static PrimitiveTypeParser buildStandardCopy() {
PrimitiveTypeParser result = new PrimitiveTypeParser();
result.getTypeParsers().add(jodaTimeAutomaticParsing);
result.getTypeParsers().add(durationAutomaticParsing);
result.getTypeParsers().add(numericParser);
result.getTypeParsers().add(decimalParser);
result.getTypeParsers().add(booleanParser);
result.getTypeParsers().add(yesNoBooleanParser);
return result;
}

private List<Function<String, Object>> typeParsers;

public PrimitiveTypeParser() {
typeParsers = new ArrayList<>();
}

public Object parseWithFallback(String input, Object fallback) {
if (input == null || input.trim().isBlank()) {
return fallback;
} else {
Object parsedValue = null;
for (Function<String, Object> parser : typeParsers) {
parsedValue = parser.apply(input);
if (parsedValue != null) {
break;
}
}

if (parsedValue == null) {
return fallback;
} else {
return parsedValue;
}
}
}

public Object parse(String input) {
return parseWithFallback(input, input);
}

public void enrichInPlace(Map<String, Object> input) {
for (Map.Entry<String, Object> entry : input.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof String) {
String stringValue = (String) value;
Object parsedValue = parse(stringValue);
input.put(key, parsedValue);
}
}
}

public Map<String, Object> parseAndCopy(Map<String, ?> input) {
Map<String, Object> result = new HashMap<>();
for (Map.Entry<String, ?> entry : input.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof String) {
String stringValue = (String) value;
Object parsedValue = parse(stringValue);
result.put(key, parsedValue);
}
}
return result;
}

public Map<String, Object> parseAndCopy(Map<String, ?> input, Function<String, String> keyMapper) {
Map<String, Object> result = new HashMap<>();
for (Map.Entry<String, ?> entry : input.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof String) {
String stringValue = (String) value;
Object parsedValue = parse(stringValue);
result.put(keyMapper.apply(key), parsedValue);
}
}
return result;
}
}
112 changes: 112 additions & 0 deletions Java/SuperUtilities/src/test/java/PrimitiveTypeParserTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import com.nuix.superutilities.misc.PrimitiveTypeParser;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

public class PrimitiveTypeParserTests extends TestFoundation {
@Test
public void testTypeParsing() throws Exception {
PrimitiveTypeParser standardTypeParser = PrimitiveTypeParser.getStandard();

// Since we are testing some without millis precision, we trim millis off so
// round trip still succeeds
DateTime now = DateTime.now(DateTimeZone.UTC).millisOfSecond().setCopy(0);
List<String> formatStrings = List.of(
"yyyy-MM-dd'T'HH:mm:ss.SSSZZ",
"yyyy-MM-dd'T'HH:mm:ss'Z'"
);

for (String formatString : formatStrings) {
log.info(formatString + " => " + now.toString(formatString));
}

for (String formatString : formatStrings) {
String inputString = now.toString(formatString);
Object parsedObject = standardTypeParser.parse(inputString);
assertNotNull(parsedObject);
assertInstanceOf(DateTime.class, parsedObject);
assertTrue(now.isEqual((DateTime) parsedObject),
String.format("Format String: %s, Expected: %s, Got: %s",
formatString, now, ((DateTime) parsedObject)));
}

List<String> additionalDates = List.of(
"2022-04-20T17:12:37Z"
);

for (String dateToParse : additionalDates) {
Object parsedDate = standardTypeParser.parse(dateToParse);
assertTrue(parsedDate instanceof DateTime, String.format("%s did not parse into a DateTime", dateToParse));
}

assertEquals("cat", standardTypeParser.parse("cat"));
assertEquals(" cat ", standardTypeParser.parse(" cat "));
assertEquals("dog", standardTypeParser.parseWithFallback("", "dog"));
assertEquals(0, standardTypeParser.parseWithFallback("bird", 0));

assertEquals(true, standardTypeParser.parse("true"));
assertEquals(false, standardTypeParser.parse("false"));
assertEquals(true, standardTypeParser.parse("True"));
assertEquals(false, standardTypeParser.parse("False"));
assertEquals(true, standardTypeParser.parse(" True "));
assertEquals(false, standardTypeParser.parse(" False "));

assertEquals(0L, standardTypeParser.parse("0"));
assertEquals(0L, standardTypeParser.parse("-0"));
assertEquals(0L, standardTypeParser.parse("+0"));
assertEquals(123456789L, standardTypeParser.parse("123456789"));
assertEquals(123456789L, standardTypeParser.parse("0123456789"));
assertEquals(123456789L, standardTypeParser.parse("+0123456789"));
assertEquals(-123456789L, standardTypeParser.parse("-0123456789"));

assertEquals(0L, standardTypeParser.parse(" 0 "));
assertEquals(0L, standardTypeParser.parse(" -0 "));
assertEquals(0L, standardTypeParser.parse(" +0 "));
assertEquals(123456789L, standardTypeParser.parse(" 123456789 "));
assertEquals(123456789L, standardTypeParser.parse(" 0123456789 "));
assertEquals(123456789L, standardTypeParser.parse(" +0123456789 "));
assertEquals(-123456789L, standardTypeParser.parse(" -0123456789 "));

assertEquals(0.0d, standardTypeParser.parse("0."));
assertEquals(-0.0d, standardTypeParser.parse("-0."));
assertEquals(0.0d, standardTypeParser.parse("+0."));
assertEquals(0.0d, standardTypeParser.parse("0.0"));
assertEquals(0.0d, standardTypeParser.parse("+0.0"));
assertEquals(-0.0d, standardTypeParser.parse("-0.0"));
assertEquals(123456789d, standardTypeParser.parse("123456789."));
assertEquals(123456789d, standardTypeParser.parse("+123456789."));
assertEquals(-123456789d, standardTypeParser.parse("-123456789."));
assertEquals(123456789d, standardTypeParser.parse("123456789.0"));
assertEquals(123456789d, standardTypeParser.parse("+123456789.0"));
assertEquals(-123456789d, standardTypeParser.parse("-123456789.0"));
assertEquals(123456789d, standardTypeParser.parse("123456789.000"));
assertEquals(123456789d, standardTypeParser.parse("+123456789.000"));
assertEquals(-123456789d, standardTypeParser.parse("-123456789.000"));
assertEquals(123456789.314d, standardTypeParser.parse("123456789.314"));
assertEquals(123456789.314d, standardTypeParser.parse("+123456789.314"));
assertEquals(-123456789.314d, standardTypeParser.parse("-123456789.314"));

assertEquals(0.0d, standardTypeParser.parse(" 0. "));
assertEquals(-0.0d, standardTypeParser.parse(" -0. "));
assertEquals(0.0d, standardTypeParser.parse(" +0. "));
assertEquals(0.0d, standardTypeParser.parse(" 0.0 "));
assertEquals(0.0d, standardTypeParser.parse(" +0.0 "));
assertEquals(-0.0d, standardTypeParser.parse(" -0.0 "));
assertEquals(123456789d, standardTypeParser.parse(" 123456789. "));
assertEquals(123456789d, standardTypeParser.parse(" +123456789. "));
assertEquals(-123456789d, standardTypeParser.parse(" -123456789. "));
assertEquals(123456789d, standardTypeParser.parse(" 123456789.0 "));
assertEquals(123456789d, standardTypeParser.parse(" +123456789.0 "));
assertEquals(-123456789d, standardTypeParser.parse(" -123456789.0 "));
assertEquals(123456789d, standardTypeParser.parse(" 123456789.000 "));
assertEquals(123456789d, standardTypeParser.parse(" +123456789.000 "));
assertEquals(-123456789d, standardTypeParser.parse(" -123456789.000 "));
assertEquals(123456789.314d, standardTypeParser.parse(" 123456789.314 "));
assertEquals(123456789.314d, standardTypeParser.parse(" +123456789.314 "));
assertEquals(-123456789.314d, standardTypeParser.parse(" -123456789.314 "));
}
}

0 comments on commit 3790d19

Please sign in to comment.