Stringable.java

/*
 * Copyright (c) 2020, Stein Eldar Johnsen
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 net.morimekta.strings;

import net.morimekta.strings.internal.DecimalFormatUtil;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Map;
import java.util.Set;

import static net.morimekta.strings.Displayable.displayableDateTime;
import static net.morimekta.strings.Displayable.displayableDuration;
import static net.morimekta.strings.Displayable.displayableInstant;
import static net.morimekta.strings.EscapeUtil.javaEscape;

/**
 * A class that can be made into a string. This is meant to be
 * a concise and meaningful, but not necessarily human understandable
 * string value, rather e.g. to make a portable non-platform bound
 * string value of an object.
 */
public interface Stringable {
    /**
     * Make a string representation of the instance value.
     *
     * @return The string representation.
     */
    String asString();

    /**
     * Make a printable string from a collection using the tools here.
     *
     * @param collection The collection to stringify.
     * @return The collection string value.
     */
    static String asString(Collection<?> collection) {
        if (collection == null) {
            return NULL;
        }
        StringBuilder builder = new StringBuilder();
        builder.append(collection instanceof Set ? '{' : '[');
        boolean first = true;
        for (Object item : collection) {
            if (first) {
                first = false;
            } else {
                builder.append(',');
            }
            builder.append(asString(item));
        }
        builder.append(collection instanceof Set ? '}' : ']');
        return builder.toString();
    }

    /**
     * Make a minimal printable string value from a typed map.
     *
     * @param map The map to stringify.
     * @return The resulting string.
     */
    static String asString(Map<?, ?> map) {
        if (map == null) {
            return NULL;
        }
        StringBuilder builder = new StringBuilder();
        builder.append('{');
        boolean first = true;
        for (Map.Entry<?, ?> entry : map.entrySet()) {
            if (first) {
                first = false;
            } else {
                builder.append(',');
            }
            builder.append(asString(entry.getKey()))
                   .append(':')
                   .append(asString(entry.getValue()));
        }
        builder.append('}');
        return builder.toString();
    }

    /**
     * @param duration The duration to stringify.
     * @return The duration string.
     */
    static String asString(Duration duration) {
        return "Duration{" + displayableDuration(duration) + "}";
    }

    /**
     * @param localDateTime The date-time to stringify.
     * @return The LocalDateTime string.
     */
    static String asString(LocalDateTime localDateTime) {
        return "LocalDateTime{" + displayableDateTime(localDateTime) + "}";
    }

    /**
     * @param zonedDateTime The date-time to stringify.
     * @return The ZonedDateTime string.
     */
    static String asString(ZonedDateTime zonedDateTime) {
        return "ZonedDateTime{" + displayableDateTime(zonedDateTime) + "}";
    }

    /**
     * @param offsetDateTime The date-time to stringify.
     * @return The OffsetDateTime string.
     */
    static String asString(OffsetDateTime offsetDateTime) {
        return "OffsetDateTime{" + displayableDateTime(offsetDateTime) + "}";
    }

    /**
     * @param instant The instant to stringify.
     * @return The Instant string.
     */
    static String asString(Instant instant) {
        return "Instant{" + displayableInstant(instant) + "}";
    }

    /**
     * Make an object into a string using the typed tools here.
     *
     * @param o The object to stringify.
     * @return The resulting string.
     */
    static String asString(Object o) {
        if (o == null) {
            return NULL;
        } else if (o instanceof Stringable) {
            return ((Stringable) o).asString();
        } else if (o instanceof CharSequence) {
            return String.format("\"%s\"", javaEscape((CharSequence) o));
        } else if (o instanceof Double) {
            return asString(((Number) o).doubleValue());
        } else if (o instanceof Float) {
            return asString(((Number) o).floatValue());
        } else if (o instanceof Collection) {
            return asString((Collection<?>) o);
        } else if (o instanceof Map) {
            return asString((Map<?, ?>) o);
        } else if (o instanceof Duration) {
            return asString((Duration) o);
        } else if (o instanceof LocalDateTime) {
            return asString((LocalDateTime) o);
        } else if (o instanceof OffsetDateTime) {
            return asString((OffsetDateTime) o);
        } else if (o instanceof ZonedDateTime) {
            return asString((ZonedDateTime) o);
        } else if (o instanceof Instant) {
            return asString((Instant) o);
        } else {
            return o.toString();
        }
    }

    /**
     * Make a minimal printable string from a double value. This method does
     * not necessary generate a string that when parsed generates the identical
     * number as given in. But ut should consistently generate the same string
     * (locale independent) for the same number with reasonable accuracy.
     * <p>
     * The float variant is pretty complex as we have to choose a printing format
     * that rounds just the right number of decimals to remove the float-rounding
     * error.
     *
     * @param f The float value.
     * @return The string value.
     */
    static String asString(float f) {
        return DecimalFormatUtil.formatFloat(f);
    }

    /**
     * Make a minimal printable string from a double value. This method does
     * not necessary generate a string that when parsed generates the identical
     * number as given in. But ut should consistently generate the same string
     * (locale independent) for the same number with reasonable accuracy.
     * <p>
     * The float variant is pretty complex as we have to choose a printing format
     * that rounds just the right number of decimals to remove the double-rounding
     * error when using extreme precisions.
     *
     * @param d The double value.
     * @return The string value.
     */
    static String asString(double d) {
        return DecimalFormatUtil.formatDouble(d);
    }

    /** The 'null' string. */
    String NULL = "null";
}