eo-runtime/src/main/java/EOorg/EOeolang/EOtxt/EOsprintf.java

Summary

Maintainability
A
3 hrs
Test Coverage
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016-2024 Objectionary.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/*
 * @checkstyle PackageNameCheck (4 lines)
 * @checkstyle TrailingCommentCheck (3 lines)
 */
package EOorg.EOeolang.EOtxt; // NOPMD

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringJoiner;
import java.util.function.Function;
import org.eolang.AtVoid;
import org.eolang.Atom;
import org.eolang.Data;
import org.eolang.Dataized;
import org.eolang.ExFailure;
import org.eolang.PhDefault;
import org.eolang.Phi;
import org.eolang.XmirObject;

/**
 * Sprintf.
 * @since 0.39.0
 * @checkstyle TypeNameCheck (5 lines)
 */
@XmirObject(oname = "sprintf")
@SuppressWarnings("PMD.AvoidDollarSigns")
public final class EOsprintf extends PhDefault implements Atom {
    /**
     * Character conversion.
     */
    private static final Map<Character, Function<Dataized, Object>> CONVERSION = new HashMap<>();

    static {
        EOsprintf.CONVERSION.put('s', Dataized::asString);
        EOsprintf.CONVERSION.put('d', element -> element.asNumber().longValue());
        EOsprintf.CONVERSION.put('f', Dataized::asNumber);
        EOsprintf.CONVERSION.put('x', element -> EOsprintf.bytesToHex(element.take()));
        EOsprintf.CONVERSION.put('b', Dataized::asBool);
    }

    /**
     * Percent sign.
     */
    private static final char PERCENT = '%';

    /**
     * Ctor.
     */
    @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors")
    public EOsprintf() {
        this.add("format", new AtVoid("format"));
        this.add("args", new AtVoid("args"));
    }

    @Override
    public Phi lambda() throws Exception {
        final String format = new Dataized(this.take("format")).asString();
        final Phi args = this.take("args");
        final Phi retriever = args.take("at");
        final long length = new Dataized(args.take("length")).asNumber().longValue();
        final List<Object> arguments = new ArrayList<>(0);
        String pattern = format;
        long index = 0;
        while (true) {
            final int idx = pattern.indexOf(EOsprintf.PERCENT);
            if (idx == -1) {
                break;
            }
            if (index == length) {
                throw new ExFailure(
                    String.format(
                        "The amount of arguments %d does not match the amount of format occurrences %d",
                        length,
                        EOsprintf.formats(format)
                    )
                );
            }
            final char sym = pattern.charAt(idx + 1);
            if (sym == EOsprintf.PERCENT) {
                pattern = pattern.substring(idx + 1);
            } else {
                final Phi taken = retriever.copy();
                taken.put(0, new Data.ToPhi(index));
                arguments.add(EOsprintf.formatted(sym, new Dataized(taken)));
                ++index;
                pattern = pattern.substring(idx + 2);
            }
        }
        return new ToPhi(
            String.format(
                Locale.US,
                format.replaceAll("%x", "%s"),
                arguments.toArray()
            )
        );
    }

    /**
     * Convert byte array to hex string.
     * @param bytes Byte array
     * @return Bytes as hex string
     */
    private static String bytesToHex(final byte[] bytes) {
        final StringJoiner out = new StringJoiner("-");
        for (final byte bty : bytes) {
            out.add(String.format("%02X", bty));
        }
        return out.toString();
    }

    /**
     * Format given {@code element} depending on format char.
     * @param symbol Format char
     * @param element Element ready for formatting
     * @return Formatted object
     */
    private static Object formatted(final char symbol, final Dataized element) {
        if (!EOsprintf.CONVERSION.containsKey(symbol)) {
            throw new ExFailure(
                String.format(
                    "The format %c is unsupported, only %s formats can be used",
                    symbol, "%s, %d, %f, %x, %b"
                )
            );
        }
        return EOsprintf.CONVERSION.get(symbol).apply(element);
    }

    /**
     * Count amount of format occurrences.
     * @param str Given string
     * @return Amount of formats in string
     */
    private static int formats(final String str) {
        int count = 0;
        for (int idx = 0; idx < str.length(); ++idx) {
            if (
                str.charAt(idx) == EOsprintf.PERCENT
                    && idx + 1 != str.length()
                    && EOsprintf.CONVERSION.containsKey(str.charAt(idx + 1))
            ) {
                ++count;
            }
        }
        return count;
    }
}