MCPDUtil.java

/**
 * Copyright 2014 Global Crop Diversity Trust
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/

package org.genesys.util;

import java.time.DateTimeException;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;

public class MCPDUtil {
	static Pattern mcpdSplit = Pattern.compile("\\s*;\\s*");

	/**
	 * @deprecated Use {@link #isMcpdDate(String)} instead; this pattern does not
	 *             enforce calendar validity or a minimum year.
	 */
	@Deprecated
	public static final Pattern MCPDDATE_PATTERN = Pattern.compile("^(?:(?:[1-9]\\d{3})|(?:----)|(?:0000))(?:--|(?:0[0-9])|(?:1[0-2]))(?:--|(?:0[0-9])|(?:[1-2][0-9])|(?:3[0-1]))$");
	public static final Pattern WIEWSCODE_PATTERN = Pattern.compile("^\\p{Upper}{3}(\\d){3,4}$");

	private static final Pattern YEAR_ONLY = Pattern.compile("^\\d{4}$");
	private static final Pattern MONTH_PART = Pattern.compile("^(0[1-9]|1[0-2])$");
	private static final Pattern DAY_PART = Pattern.compile("^(0[1-9]|[12]\\d|3[01])$");
	private static final String MISSING = "--";
	private static final String MISSING_ZERO = "00";


	/**
	 * Convert a List to MCPD "array string" with semicolon as separator.
	 * 
	 * @param integers
	 * @return
	 */
	public static String toMcpdArray(Collection<?> objects) {
		if (objects == null || objects.size() == 0)
			return null;

		StringBuilder sb = new StringBuilder();

		objects.stream().filter(i -> i != null).forEach(i -> {
			if (sb.length() > 0)
				sb.append(";");
			sb.append(i);
		});

		return StringUtils.defaultIfBlank(sb.toString(), null);
	}

	/**
	 * Convert an MCPD "array" (e.g. "10;20") to a {@link List} of Integers,
	 * excluding blank and null elements.
	 * 
	 * @throws NumberFormatException
	 *             if the input string contains non-numeric elements.
	 */
	public static List<Integer> toIntegers(String ints) {
		if (StringUtils.isBlank(ints))
			return null;
		return Arrays.stream(mcpdSplit.split(ints)).filter(str -> StringUtils.isNotBlank(str)).map(i -> Integer.parseInt(i)).collect(Collectors.toList());
	}

	/**
	 * Convert an MCPD "array string" (e.g. "NOR051;AUS033") to a {@link List},
	 * excluding blank and null elements.
	 * 
	 * @param strings
	 * @return
	 */
	public static List<String> toList(String strings) {
		if (StringUtils.isBlank(strings))
			return null;
		return Arrays.stream(mcpdSplit.split(strings)).filter(str -> StringUtils.isNotBlank(str)).collect(Collectors.toList());
	}

	/**
	 * Validates a canonical 8-character MCPD date, including calendar correctness.
	 *
	 * @param date the date string
	 * @return {@code true} only if the value is non-blank, exactly 8 characters,
	 *         has a 4-digit year {@code >= 1600}, a valid month or month
	 *         placeholder, and a valid day or day placeholder. Day is only
	 *         allowed when month is provided.
	 */
	public static boolean isMcpdDate(String date) {
		return parseValidMcpdDate(date) != null;
	}

	private static LocalDate parseValidMcpdDate(String date) {
		if (StringUtils.isBlank(date) || date.length() != 8) {
			return null;
		}

		String yearPart = date.substring(0, 4);
		String monthPart = date.substring(4, 6);
		String dayPart = date.substring(6, 8);

		if (!YEAR_ONLY.matcher(yearPart).matches()) {
			return null;
		}
		int year = Integer.parseInt(yearPart);
		if (year < 1600) {
			return null;
		}

		boolean monthMissing = MISSING.equals(monthPart) || MISSING_ZERO.equals(monthPart);
		boolean dayMissing = MISSING.equals(dayPart) || MISSING_ZERO.equals(dayPart);

		if (!monthMissing) {
			if (!MONTH_PART.matcher(monthPart).matches()) {
				return null;
			}
		}
		if (!dayMissing) {
			if (monthMissing) {
				return null;
			}
			if (!DAY_PART.matcher(dayPart).matches()) {
				return null;
			}
		}

		if (!monthMissing && dayMissing) {
			return LocalDate.of(year, Integer.parseInt(monthPart), 1);
		}
		if (monthMissing && dayMissing) {
			return LocalDate.of(year, 1, 1);
		}
		if (!monthMissing && !dayMissing) {
			try {
				return LocalDate.of(year, Integer.parseInt(monthPart), Integer.parseInt(dayPart));
			} catch (DateTimeException e) {
				return null;
			}
		}
		// no match
		return null;
	}

	/**
	 * Returns true when the value has the 8-character digit-or-dash shape used by
	 * canonical MCPD dates. This does not check calendar validity; placeholder-only
	 * strings such as {@code --------} or {@code 00000000} are accepted.
	 */
	public static boolean hasMcpdDateSyntax(String date) {
		if (StringUtils.isBlank(date) || date.length() != 8) {
			return false;
		}
		for (int i = 0; i < date.length(); i++) {
			char c = date.charAt(i);
			if (c != '-' && (c < '0' || c > '9')) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Nulls out blank values, values that do not have 8-character digit-or-dash
	 * syntax, and MCPD dates with a placeholder year ({@code ----} or {@code 0000}).
	 * A "real" date requires a non-placeholder year and at least one non-zero digit.
	 */
	public static String cleanMcpdDate(String date) {
		if (StringUtils.isBlank(date)) {
			return null;
		}
		date = date.trim();
		if (!hasMcpdDateSyntax(date)) {
			return null;
		}
		if (Strings.CI.startsWith("----", date) || Strings.CI.startsWith("0000", date)) {
			return null;
		}
		boolean hasRealValue = false;
		for (int i = 0; i < date.length(); i++) {
			char c = date.charAt(i);
			if (c != '-' && c != '0') {
				hasRealValue = true;
				break;
			}
		}
		return hasRealValue ? date : null;
	}

	public static boolean isWiewsCode(String code) {
		return code == null ? false : WIEWSCODE_PATTERN.matcher(code).matches();
	}
}