stream, Object monitor) {
+		Objects.requireNonNull(stream, "stream cannot be null");
+		return new SyncStream<>(stream, monitor);
+	}
+
+	/**
+	 * Wraps the given {@link IntStream} to make all
+	 * 
+	 * terminal operations acquire the provided monitor's lock before
+	 * execution. Intermediate operations
+	 * return streams that are also synchronized on the same object. The created
+	 * stream will behave identically
+	 * to the provided stream in all other aspects. Use this to synchronize
+	 * access to stream's source.
+	 * 
+	 * The returned {@code IntStream}'s {@link IntStream#iterator()
+	 * iterator()} and
+	 * {@link IntStream#spliterator() spliterator()} methods return regular
+	 * non-synchronized iterators and
+	 * spliterators respectively. It is the user's responsibility to avoid
+	 * concurrency issues:
+	 * 
+	 * 
+	 * synchronized (stream.getMonitor()) {
+	 *     PrimitiveIterator.OfInt it = stream.iterator();
+	 *         ...
+	 * }
+	 * 
+	 * 
+	 * Usage example:
+	 * 
+	 * 
+	 * Set<Object> s = Collections.synchronizedSet(new HashSet<>());
+	 *    ...
+	 * IntStream stream = SyncStreams.synchronizedStream(s.stream().mapToInt(Object::hashCode), s);
+	 * stream = stream.map(i -> i % 67); // Still synchronized
+	 * stream.forEach(System.out::println); // Should never throw a ConcurrentModificationException
+	 * 
+	 * 
+	 * @param stream  the stream to wrap.
+	 * @param monitor the object that the stream will use for synchronization.
+	 *                When {@code null}, the stream
+	 *                will synchronize on itself.
+	 * @return a {@link SyncIntStream} synchronized on {@code monitor} and
+	 *         backed by {@code stream}.
+	 * @throws NullPointerException if {@code stream == null}.
+	 */
+	public static SyncIntStream synchronizedStream(IntStream stream, Object monitor) {
+		Objects.requireNonNull(stream, "stream cannot be null");
+		return new SyncIntStream(stream, monitor);
+	}
+
+	/**
+	 * Wraps the given {@link LongStream} to make all
+	 * 
+	 * terminal operations acquire the provided monitor's lock before
+	 * execution. Intermediate operations
+	 * return streams that are also synchronized on the same object. The created
+	 * stream will behave identically
+	 * to the provided stream in all other aspects. Use this to synchronize
+	 * access to stream's source.
+	 * 
+	 * The returned {@code LongStream}'s {@link LongStream#iterator()
+	 * iterator()} and
+	 * {@link LongStream#spliterator() spliterator()} methods return regular
+	 * non-synchronized iterators and
+	 * spliterators respectively. It is the user's responsibility to avoid
+	 * concurrency issues:
+	 * 
+	 * 
+	 * synchronized (stream.getMonitor()) {
+	 *     PrimitiveIterator.OfLong it = stream.iterator();
+	 *         ...
+	 * }
+	 * 
+	 * 
+	 * Usage example:
+	 * 
+	 * 
+	 * Set<Object> s = Collections.synchronizedSet(new HashSet<>());
+	 *    ...
+	 * LongStream stream = SyncStreams.synchronizedStream(s.stream().mapToLong(o -> (long) o.hashCode()), s);
+	 * stream = stream.map(i -> i % 67); // Still synchronized
+	 * stream.forEach(System.out::println); // Should never throw a ConcurrentModificationException
+	 * 
+	 * 
+	 * @param stream  the stream to wrap.
+	 * @param monitor the object that the stream will use for synchronization.
+	 *                When {@code null}, the stream
+	 *                will synchronize on itself.
+	 * @return a {@link SyncLongStream} synchronized on {@code monitor} and
+	 *         backed by {@code stream}.
+	 * @throws NullPointerException if {@code stream == null}.
+	 */
+	public static SyncLongStream synchronizedStream(LongStream stream, Object monitor) {
+		Objects.requireNonNull(stream, "stream cannot be null");
+		return new SyncLongStream(stream, monitor);
+	}
+
+	/**
+	 * Wraps the given {@link DoubleStream} to make all
+	 * 
+	 * terminal operations acquire the provided monitor's lock before
+	 * execution. Intermediate operations
+	 * return streams that are also synchronized on the same object. The created
+	 * stream will behave identically
+	 * to the provided stream in all other aspects. Use this to synchronize
+	 * access to stream's source.
+	 * 
+	 * The returned {@code DoubleStream}'s {@link DoubleStream#iterator()
+	 * iterator()} and
+	 * {@link DoubleStream#spliterator() spliterator()} methods return regular
+	 * non-synchronized iterators and
+	 * spliterators respectively. It is the user's responsibility to avoid
+	 * concurrency issues:
+	 * 
+	 * 
+	 * synchronized (stream.getMonitor()) {
+	 *     PrimitiveIterator.OfDouble it = stream.iterator();
+	 *         ...
+	 * }
+	 * 
+	 * 
+	 * Usage example:
+	 * 
+	 * 
+	 * Set<Object> s = Collections.synchronizedSet(new HashSet<>());
+	 *    ...
+	 * DoubleStream stream = SyncStreams.synchronizedStream(s.stream().mapToLong(o -> (double) o.hashCode()), s);
+	 * stream = stream.map(Math::sin); // Still synchronized
+	 * stream.forEach(System.out::println); // Should never throw a ConcurrentModificationException
+	 * 
+	 * 
+	 * @param stream  the stream to wrap.
+	 * @param monitor the object that the stream will use for synchronization.
+	 *                When {@code null}, the stream
+	 *                will synchronize on itself.
+	 * @return a {@link SyncDoubleStream} synchronized on {@code monitor} and
+	 *         backed by {@code stream}.
+	 * @throws NullPointerException if {@code stream == null}.
+	 */
+	public static SyncDoubleStream synchronizedStream(DoubleStream stream, Object monitor) {
+		Objects.requireNonNull(stream, "stream cannot be null");
+		return new SyncDoubleStream(stream, monitor);
+	}
+
+	/*
+	 * Private constructor
+	 */
+	private SyncStreams() {
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/SyntaxException.java b/src/main/java/ru/windcorp/jputil/SyntaxException.java
index e47d123..02cc3c4 100644
--- a/src/main/java/ru/windcorp/jputil/SyntaxException.java
+++ b/src/main/java/ru/windcorp/jputil/SyntaxException.java
@@ -1,44 +1,45 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil;
-
-public class SyntaxException extends Exception {
-
-	private static final long serialVersionUID = -4052144233640072750L;
-
-	public SyntaxException() {
-		
-	}
-
-	public SyntaxException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
-		super(message, cause, enableSuppression, writableStackTrace);
-	}
-
-	public SyntaxException(String message, Throwable cause) {
-		super(message, cause);
-	}
-
-	public SyntaxException(String message) {
-		super(message);
-	}
-
-	public SyntaxException(Throwable cause) {
-		super(cause);
-	}
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil;
+
+public class SyntaxException extends Exception {
+
+	private static final long serialVersionUID = -4052144233640072750L;
+
+	public SyntaxException() {
+
+	}
+
+	public SyntaxException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+		super(message, cause, enableSuppression, writableStackTrace);
+	}
+
+	public SyntaxException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	public SyntaxException(String message) {
+		super(message);
+	}
+
+	public SyntaxException(Throwable cause) {
+		super(cause);
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/CharArrayIterator.java b/src/main/java/ru/windcorp/jputil/chars/CharArrayIterator.java
index f65ddf4..be0940d 100644
--- a/src/main/java/ru/windcorp/jputil/chars/CharArrayIterator.java
+++ b/src/main/java/ru/windcorp/jputil/chars/CharArrayIterator.java
@@ -1,128 +1,132 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars;
-
-import java.text.CharacterIterator;
-
-public class CharArrayIterator implements CharacterIterator {
-
-	private final char[] array;
-	private int pos;
-
-	public CharArrayIterator(char[] array) {
-		this.array = array;
-	}
-
-	public CharArrayIterator(String src) {
-		this(src.toCharArray());
-	}
-
-	@Override
-	public char first() {
-		pos = 0;
-		if (array.length != 0) {
-			return array[pos];
-		}
-		return DONE;
-	}
-
-	@Override
-	public char last() {
-		pos = array.length;
-		if (array.length != 0) {
-			pos -= 1;
-			return array[pos];
-		}
-		return DONE;
-	}
-
-	@Override
-	public char current() {
-		if (array.length != 0 && pos < array.length) {
-			return array[pos];
-		}
-		return DONE;
-	}
-
-	@Override
-	public char next() {
-		pos += 1;
-		if (pos >= array.length) {
-			pos = array.length;
-			return DONE;
-		}
-		return current();
-	}
-
-	@Override
-	public char previous() {
-		if (pos == 0) {
-			return DONE;
-		}
-		pos -= 1;
-		return current();
-	}
-
-	@Override
-	public char setIndex(int position) {
-		if (position < 0 || position > array.length) {
-			throw new IllegalArgumentException("bad position: " + position);
-		}
-
-		pos = position;
-
-		if (pos != array.length && array.length != 0) {
-			return array[pos];
-		}
-		return DONE;
-	}
-
-	@Override
-	public int getBeginIndex() {
-		return 0;
-	}
-
-	@Override
-	public int getEndIndex() {
-		return array.length;
-	}
-
-	@Override
-	public int getIndex() {
-		return pos;
-	}
-	
-//	@SuppressWarnings("all") Just STFU, this _is_ terrific
-	
-	// SonarLint: "clone" should not be overridden (java:S2975)
-	//   And I wouldn't have done that if only CharacterIterator had not required exception safety.
-	// SonarLint: "toString()" and "clone()" methods should not return null (java:S2225)
-	//   The clause is unreachable: CharacterArrayIterator implements Cloneable and superclass is Object.
-	@SuppressWarnings({"squid:S2975", "squid:S2225"})
-
-	@Override
-	public CharArrayIterator clone() {
-		try {
-			return (CharArrayIterator) super.clone();
-		} catch (CloneNotSupportedException cnse) {
-			// Impossible
-			return null;
-		}
-	}
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars;
+
+import java.text.CharacterIterator;
+
+public class CharArrayIterator implements CharacterIterator {
+
+	private final char[] array;
+	private int pos;
+
+	public CharArrayIterator(char[] array) {
+		this.array = array;
+	}
+
+	public CharArrayIterator(String src) {
+		this(src.toCharArray());
+	}
+
+	@Override
+	public char first() {
+		pos = 0;
+		if (array.length != 0) {
+			return array[pos];
+		}
+		return DONE;
+	}
+
+	@Override
+	public char last() {
+		pos = array.length;
+		if (array.length != 0) {
+			pos -= 1;
+			return array[pos];
+		}
+		return DONE;
+	}
+
+	@Override
+	public char current() {
+		if (array.length != 0 && pos < array.length) {
+			return array[pos];
+		}
+		return DONE;
+	}
+
+	@Override
+	public char next() {
+		pos += 1;
+		if (pos >= array.length) {
+			pos = array.length;
+			return DONE;
+		}
+		return current();
+	}
+
+	@Override
+	public char previous() {
+		if (pos == 0) {
+			return DONE;
+		}
+		pos -= 1;
+		return current();
+	}
+
+	@Override
+	public char setIndex(int position) {
+		if (position < 0 || position > array.length) {
+			throw new IllegalArgumentException("bad position: " + position);
+		}
+
+		pos = position;
+
+		if (pos != array.length && array.length != 0) {
+			return array[pos];
+		}
+		return DONE;
+	}
+
+	@Override
+	public int getBeginIndex() {
+		return 0;
+	}
+
+	@Override
+	public int getEndIndex() {
+		return array.length;
+	}
+
+	@Override
+	public int getIndex() {
+		return pos;
+	}
+
+//	@SuppressWarnings("all") Just STFU, this _is_ terrific
+
+	// SonarLint: "clone" should not be overridden (java:S2975)
+	// And I wouldn't have done that if only CharacterIterator had not required
+	// exception safety.
+	// SonarLint: "toString()" and "clone()" methods should not return null
+	// (java:S2225)
+	// The clause is unreachable: CharacterArrayIterator implements Cloneable
+	// and superclass is Object.
+	@SuppressWarnings({ "squid:S2975", "squid:S2225" })
+
+	@Override
+	public CharArrayIterator clone() {
+		try {
+			return (CharArrayIterator) super.clone();
+		} catch (CloneNotSupportedException cnse) {
+			// Impossible
+			return null;
+		}
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/CharConsumer.java b/src/main/java/ru/windcorp/jputil/chars/CharConsumer.java
index de1d372..7b68bb1 100644
--- a/src/main/java/ru/windcorp/jputil/chars/CharConsumer.java
+++ b/src/main/java/ru/windcorp/jputil/chars/CharConsumer.java
@@ -1,42 +1,43 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars;
-
-import java.util.function.IntConsumer;
-
-@FunctionalInterface
-public interface CharConsumer {
-
-	void accept(char c);
-	
-	public static CharConsumer andThen(CharConsumer first, CharConsumer second) {
-		return c -> {
-			first.accept(c);
-			second.accept(c);
-		};
-	}
-	
-	public static IntConsumer toInt(CharConsumer consumer) {
-		return i -> consumer.accept((char) i);
-	}
-	
-	public static CharConsumer toChar(IntConsumer consumer) {
-		return consumer::accept;
-	}
-	
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars;
+
+import java.util.function.IntConsumer;
+
+@FunctionalInterface
+public interface CharConsumer {
+
+	void accept(char c);
+
+	public static CharConsumer andThen(CharConsumer first, CharConsumer second) {
+		return c -> {
+			first.accept(c);
+			second.accept(c);
+		};
+	}
+
+	public static IntConsumer toInt(CharConsumer consumer) {
+		return i -> consumer.accept((char) i);
+	}
+
+	public static CharConsumer toChar(IntConsumer consumer) {
+		return consumer::accept;
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/CharConsumers.java b/src/main/java/ru/windcorp/jputil/chars/CharConsumers.java
index e0613bc..1ac7973 100644
--- a/src/main/java/ru/windcorp/jputil/chars/CharConsumers.java
+++ b/src/main/java/ru/windcorp/jputil/chars/CharConsumers.java
@@ -1,69 +1,70 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars;
-
-import java.util.Objects;
-
-import ru.windcorp.jputil.ArrayUtil;
-
-/**
- * @author Javapony
- *
- */
-public class CharConsumers {
-	
-	private CharConsumers() {}
-	
-	public static CharConsumer fillArray(char[] array, int offset, int length) {
-		return new ArrayFiller(array, offset, length);
-	}
-	
-	public static CharConsumer fillArray(char[] array) {
-		return fillArray(array, 0, -1);
-	}
-	
-	private static class ArrayFiller implements CharConsumer {
-		
-		final char[] array;
-		int i;
-		final int end;
-
-		/**
-		 * @param array
-		 * @param offset
-		 * @param length
-		 */
-		ArrayFiller(char[] array, int offset, int length) {
-			this.array = Objects.requireNonNull(array, "array");
-			this.end = ArrayUtil.checkArrayStartEnd(array, offset, offset + length);
-			this.i = offset;
-		}
-
-		/**
-		 * @see ru.windcorp.jputil.chars.CharConsumer#accept(char)
-		 */
-		@Override
-		public void accept(char c) {
-			if (i == end)
-				throw new ArrayIndexOutOfBoundsException(end);
-			array[i++] = c;
-		}
-
-	}
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars;
+
+import java.util.Objects;
+
+import ru.windcorp.jputil.ArrayUtil;
+
+/**
+ * @author Javapony
+ */
+public class CharConsumers {
+
+	private CharConsumers() {
+	}
+
+	public static CharConsumer fillArray(char[] array, int offset, int length) {
+		return new ArrayFiller(array, offset, length);
+	}
+
+	public static CharConsumer fillArray(char[] array) {
+		return fillArray(array, 0, -1);
+	}
+
+	private static class ArrayFiller implements CharConsumer {
+
+		final char[] array;
+		int i;
+		final int end;
+
+		/**
+		 * @param array
+		 * @param offset
+		 * @param length
+		 */
+		ArrayFiller(char[] array, int offset, int length) {
+			this.array = Objects.requireNonNull(array, "array");
+			this.end = ArrayUtil.checkArrayStartEnd(array, offset, offset + length);
+			this.i = offset;
+		}
+
+		/**
+		 * @see ru.windcorp.jputil.chars.CharConsumer#accept(char)
+		 */
+		@Override
+		public void accept(char c) {
+			if (i == end)
+				throw new ArrayIndexOutOfBoundsException(end);
+			array[i++] = c;
+		}
+
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/CharPredicate.java b/src/main/java/ru/windcorp/jputil/chars/CharPredicate.java
index b4be567..03ded02 100644
--- a/src/main/java/ru/windcorp/jputil/chars/CharPredicate.java
+++ b/src/main/java/ru/windcorp/jputil/chars/CharPredicate.java
@@ -1,84 +1,85 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars;
-
-import java.util.Arrays;
-import java.util.function.IntPredicate;
-
-import ru.windcorp.jputil.ArrayUtil;
-
-@FunctionalInterface
-public interface CharPredicate {
-
-	boolean test(char c);
-	
-	public static CharPredicate and(CharPredicate first, CharPredicate second) {
-		return c -> first.test(c) && second.test(c);
-	}
-	
-	public static CharPredicate or(CharPredicate first, CharPredicate second) {
-		return c -> first.test(c) || second.test(c);
-	}
-	
-	public static CharPredicate negate(CharPredicate predicate) {
-		return c -> !predicate.test(c);
-	}
-	
-	public static IntPredicate toInt(CharPredicate predicate) {
-		return i -> predicate.test((char) i);
-	}
-	
-	public static CharPredicate toChar(IntPredicate predicate) {
-		return predicate::test;
-	}
-	
-	public static CharPredicate forArray(char... chars) {
-		if (chars.length == 0) {
-			return c -> false;
-		}
-		
-		if (chars.length == 1) {
-			return forChar(chars[0]);
-		}
-		
-		if (chars.length < 16) {
-			return c -> ArrayUtil.firstIndexOf(chars, c) >= 0;
-		} else {
-			final char[] sorted = Arrays.copyOf(chars, chars.length);
-			Arrays.sort(sorted);
-			return c -> Arrays.binarySearch(chars, c) >= 0;
-		}
-	}
-	
-	public static CharPredicate forChar(final char c) {
-		return given -> given == c;
-	}
-	
-	public static CharPredicate forRange(final char minInclusive, final char maxExclusive) {
-		if (minInclusive > maxExclusive) {
-			throw new IllegalArgumentException("min > max: " + minInclusive + " > " + maxExclusive);
-		}
-		
-		if (minInclusive == maxExclusive) {
-			return c -> false;
-		}
-		
-		return c -> c >= minInclusive && c < maxExclusive;
-	}
-	
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars;
+
+import java.util.Arrays;
+import java.util.function.IntPredicate;
+
+import ru.windcorp.jputil.ArrayUtil;
+
+@FunctionalInterface
+public interface CharPredicate {
+
+	boolean test(char c);
+
+	public static CharPredicate and(CharPredicate first, CharPredicate second) {
+		return c -> first.test(c) && second.test(c);
+	}
+
+	public static CharPredicate or(CharPredicate first, CharPredicate second) {
+		return c -> first.test(c) || second.test(c);
+	}
+
+	public static CharPredicate negate(CharPredicate predicate) {
+		return c -> !predicate.test(c);
+	}
+
+	public static IntPredicate toInt(CharPredicate predicate) {
+		return i -> predicate.test((char) i);
+	}
+
+	public static CharPredicate toChar(IntPredicate predicate) {
+		return predicate::test;
+	}
+
+	public static CharPredicate forArray(char... chars) {
+		if (chars.length == 0) {
+			return c -> false;
+		}
+
+		if (chars.length == 1) {
+			return forChar(chars[0]);
+		}
+
+		if (chars.length < 16) {
+			return c -> ArrayUtil.firstIndexOf(chars, c) >= 0;
+		} else {
+			final char[] sorted = Arrays.copyOf(chars, chars.length);
+			Arrays.sort(sorted);
+			return c -> Arrays.binarySearch(chars, c) >= 0;
+		}
+	}
+
+	public static CharPredicate forChar(final char c) {
+		return given -> given == c;
+	}
+
+	public static CharPredicate forRange(final char minInclusive, final char maxExclusive) {
+		if (minInclusive > maxExclusive) {
+			throw new IllegalArgumentException("min > max: " + minInclusive + " > " + maxExclusive);
+		}
+
+		if (minInclusive == maxExclusive) {
+			return c -> false;
+		}
+
+		return c -> c >= minInclusive && c < maxExclusive;
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/CharSupplier.java b/src/main/java/ru/windcorp/jputil/chars/CharSupplier.java
index 580deef..0743d79 100644
--- a/src/main/java/ru/windcorp/jputil/chars/CharSupplier.java
+++ b/src/main/java/ru/windcorp/jputil/chars/CharSupplier.java
@@ -1,35 +1,36 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars;
-
-import java.util.function.IntSupplier;
-
-@FunctionalInterface
-public interface CharSupplier {
-
-	char getAsChar();
-	
-	public static IntSupplier toInt(CharSupplier consumer) {
-		return consumer::getAsChar;
-	}
-	
-	public static CharSupplier toChar(IntSupplier consumer) {
-		return () -> (char) consumer.getAsInt();
-	}
-	
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars;
+
+import java.util.function.IntSupplier;
+
+@FunctionalInterface
+public interface CharSupplier {
+
+	char getAsChar();
+
+	public static IntSupplier toInt(CharSupplier consumer) {
+		return consumer::getAsChar;
+	}
+
+	public static CharSupplier toChar(IntSupplier consumer) {
+		return () -> (char) consumer.getAsInt();
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/EscapeException.java b/src/main/java/ru/windcorp/jputil/chars/EscapeException.java
index 2578dad..935f72f 100644
--- a/src/main/java/ru/windcorp/jputil/chars/EscapeException.java
+++ b/src/main/java/ru/windcorp/jputil/chars/EscapeException.java
@@ -1,44 +1,45 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars;
-
-public class EscapeException extends Exception {
-
-	private static final long serialVersionUID = -3647188859290365053L;
-
-	public EscapeException() {
-		super();
-	}
-
-	public EscapeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
-		super(message, cause, enableSuppression, writableStackTrace);
-	}
-
-	public EscapeException(String message, Throwable cause) {
-		super(message, cause);
-	}
-
-	public EscapeException(String message) {
-		super(message);
-	}
-
-	public EscapeException(Throwable cause) {
-		super(cause);
-	}
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars;
+
+public class EscapeException extends Exception {
+
+	private static final long serialVersionUID = -3647188859290365053L;
+
+	public EscapeException() {
+		super();
+	}
+
+	public EscapeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+		super(message, cause, enableSuppression, writableStackTrace);
+	}
+
+	public EscapeException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	public EscapeException(String message) {
+		super(message);
+	}
+
+	public EscapeException(Throwable cause) {
+		super(cause);
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/Escaper.java b/src/main/java/ru/windcorp/jputil/chars/Escaper.java
index 17e9b26..f19f6c7 100644
--- a/src/main/java/ru/windcorp/jputil/chars/Escaper.java
+++ b/src/main/java/ru/windcorp/jputil/chars/Escaper.java
@@ -1,474 +1,514 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars;
-
-import java.text.CharacterIterator;
-
-import ru.windcorp.jputil.ArrayUtil;
-import ru.windcorp.jputil.chars.reader.CharReader;
-import ru.windcorp.jputil.chars.reader.CharReaders;
-
-public class Escaper {
-	
-	public static class EscaperBuilder {
-		private char escapeChar = '\\';
-		private char unicodeEscapeChar = 'u';
-		private char[] safes = null;
-		private char[] unsafes = null;
-		
-		private boolean preferUnicode = false;
-		private boolean strict = true;
-		
-		public EscaperBuilder withEscapeChar(char escapeChar) {
-			this.escapeChar = escapeChar;
-			return this;
-		}
-		
-		public EscaperBuilder withUnicodeEscapeChar(char unicodeEscapeChar) {
-			this.unicodeEscapeChar = unicodeEscapeChar;
-			return this;
-		}
-		
-		public EscaperBuilder withChars(char[] safes, char[] unsafes) {
-			this.safes = safes;
-			this.unsafes = unsafes;
-			return this;
-		}
-		
-		public EscaperBuilder withChars(String safes, String unsafes) {
-			this.safes = safes.toCharArray();
-			this.unsafes = unsafes.toCharArray();
-			return this;
-		}
-		
-		public EscaperBuilder withChars(char[] chars) {
-			this.safes = this.unsafes = chars;
-			return this;
-		}
-		
-		public EscaperBuilder withChars(String chars) {
-			this.safes = this.unsafes = chars.toCharArray();
-			return this;
-		}
-		
-		public EscaperBuilder withSafes(char[] safes) {
-			this.safes = safes;
-			return this;
-		}
-		
-		public EscaperBuilder withSafes(String safes) {
-			this.safes = safes.toCharArray();
-			return this;
-		}
-		
-		public EscaperBuilder withUnsafes(char[] unsafes) {
-			this.unsafes = unsafes;
-			return this;
-		}
-		
-		public EscaperBuilder withUnsafes(String unsafes) {
-			this.unsafes = unsafes.toCharArray();
-			return this;
-		}
-		
-		public EscaperBuilder preferUnicode(boolean preferUnicode) {
-			this.preferUnicode = preferUnicode;
-			return this;
-		}
-		
-		public EscaperBuilder strict(boolean strict) {
-			this.strict = strict;
-			return this;
-		}
-		
-		public Escaper build() {
-			return new Escaper(escapeChar, unicodeEscapeChar, safes, unsafes, preferUnicode, strict);
-		}
-		
-	}
-	
-	public static final Escaper JAVA = new Escaper('\\', 'u', "tbnrf'\"".toCharArray(), "\t\b\n\r\f\'\"".toCharArray(), true, true);
-	
-	private final char escapeChar;
-	private final char unicodeEscapeChar;
-	private final char[] safes;
-	private final char[] unsafes;
-	
-	private final boolean preferUnicode;
-	private final boolean strict;
-	
-	protected Escaper(
-			char escapeChar, char unicodeEscapeChar,
-			char[] safes, char[] unsafes,
-			boolean preferUnicode, boolean strict) {
-		this.escapeChar = escapeChar;
-		this.unicodeEscapeChar = unicodeEscapeChar;
-		this.safes = safes;
-		this.unsafes = unsafes;
-		this.preferUnicode = preferUnicode;
-		this.strict = strict;
-		
-		int duplicate;
-		if ((duplicate = ArrayUtil.hasDuplicates(safes)) != -1)
-			throw new IllegalArgumentException("Duplicate safe character '" + safes[duplicate] + "'");
-		
-		if ((duplicate = ArrayUtil.hasDuplicates(unsafes)) != -1)
-			throw new IllegalArgumentException("Duplicate unsafe character '" + unsafes[duplicate] + "'");
-		
-		for (char c : safes) {
-			if (c == escapeChar) throw new IllegalArgumentException("Safe characters contain escape chatacter");
-			if (c == unicodeEscapeChar) throw new IllegalArgumentException("Safe characters contain Unicode escape chatacter");
-		}
-		
-		for (char c : unsafes) {
-			if (c == escapeChar) throw new IllegalArgumentException("Unsafe characters contain escape chatacter (escape character is escaped automatically)");
-			if (c == unicodeEscapeChar) throw new IllegalArgumentException("Unsafe characters contain Unicode escape chatacter");
-		}
-	}
-	
-	public static EscaperBuilder create() {
-		return new EscaperBuilder();
-	}
-	
-	/*
-	 * Logic - escape
-	 */
-	
-	public void escape(CharReader src, int length, CharPredicate until, CharConsumer output) {
-		int end;
-		if (length < 0) end = Integer.MAX_VALUE;
-		else end = src.getPosition() + length;
-		while (src.has() &&
-				src.getPosition() < end &&
-				(until == null || !until.test(src.current())))
-			escape(src.consume(), output);
-	}
-	
-	public void escape(char c, CharConsumer output) {
-		if (c == escapeChar) {
-			output.accept(escapeChar);
-			output.accept(escapeChar);
-			return;
-		}
-		
-		int index = ArrayUtil.firstIndexOf(unsafes, c);
-		
-		if (index >= 0) {
-			output.accept(escapeChar);
-			output.accept(safes[index]);
-		} else {
-			if (preferUnicode && !isRegular(c)) {
-				escapeAsHex(c, output);
-			} else {
-				output.accept(c);
-			}
-		}
-	}
-	
-	// SonarLint: Assignments should not be made from within sub-expressions (java:S1121)
-	//   Seems self-evident enough
-	@SuppressWarnings("squid:S1121")
-	
-	private void escapeAsHex(char c, CharConsumer output) {
-		output.accept(escapeChar);
-		output.accept(unicodeEscapeChar);
-		output.accept(StringUtil.hexDigit(c >>= (4 * 3)));
-		output.accept(StringUtil.hexDigit(c >>= (4 * 2)));
-		output.accept(StringUtil.hexDigit(c >>= (4 * 1)));
-		output.accept(StringUtil.hexDigit(c >>  (4 * 0)));
-	}
-
-	public int getEscapedLength(CharReader src, int length, CharPredicate until) {
-		int end;
-		if (length < 0) end = Integer.MAX_VALUE;
-		else end = src.getPosition() + length;
-		
-		int result = 0;
-		
-		while (src.has() &&
-				src.getPosition() < end &&
-				(until == null || !until.test(src.current()))) {
-			result += getEscapedLength(src.consume());
-		}
-		
-		return result;
-	}
-	
-	public int getEscapedLength(char c) {
-		if (c == escapeChar || ArrayUtil.firstIndexOf(unsafes, c) >= 0)
-			return 2;
-		else {
-			if (preferUnicode && !isRegular(c))
-				return 6;
-			else
-				return 1;
-		}
-	}
-	
-	/*
-	 * Logic - unescape
-	 */
-	
-	public void unescape(CharReader src, int length, CharPredicate until, CharConsumer output) throws EscapeException {
-		int end;
-		if (length < 0) end = Integer.MAX_VALUE;
-		else end = src.getPosition() + length;
-		while (src.has() &&
-				src.getPosition() < end &&
-				(until == null || !until.test(src.current()))) {
-			output.accept(unescapeOneSequence(src));
-		}
-	}
-	
-	public char unescapeOneSequence(CharReader src) throws EscapeException {
-		int resetPos = src.getPosition();
-		try {
-			if (src.current() == escapeChar) {
-				src.next();
-				
-				if (src.isEnd())
-					throw new EscapeException("Incomplete escape sequence at the end");
-				
-				if (src.current() == escapeChar) {
-					src.next();
-					return escapeChar;
-				}
-				
-				if (src.current() == unicodeEscapeChar) {
-					src.next();
-					return (char) (
-							hexValue(src.consume()) << (4 * 3) |
-							hexValue(src.consume()) << (4 * 2) |
-							hexValue(src.consume()) << (4 * 1) |
-							hexValue(src.consume()) << (4 * 0)
-					);
-				}
-				
-				int index = ArrayUtil.firstIndexOf(safes, src.current());
-				if (index >= 0) {
-					src.next();
-					return unsafes[index];
-				}
-				
-				if (strict)
-					throw new EscapeException("Unknown escape sequence \"" + escapeChar + src.current() + "\"");
-				else
-					return src.consume();
-			} else
-				return src.consume();
-		} catch (EscapeException | RuntimeException e) {
-			src.setPosition(resetPos);
-			throw e;
-		}
-	}
-	
-	public int getUnescapedLength(CharReader src, int length, CharPredicate until) {
-		int end;
-		if (length < 0) end = Integer.MAX_VALUE;
-		else end = src.getPosition() + length;
-		
-		int result = 0;
-		
-		while (src.has() &&
-				src.getPosition() < end &&
-				(until == null || !until.test(src.current()))) {
-			skipOneSequence(src);
-			result++;
-		}
-		
-		return result;
-	}
-	
-	public void skipOneSequence(CharReader src) {
-		if (
-				src.current() == escapeChar
-				&&
-				src.next() == unicodeEscapeChar
-		) {
-			src.advance(4);
-		}
-		src.next();
-	}
-	
-	/*
-	 * Utility
-	 */
-	
-	public void escape(CharReader src, int length, CharConsumer output) {
-		escape(src, length, null, output);
-	}
-	
-	public void escape(CharReader src, CharPredicate until, CharConsumer output) {
-		escape(src, -1, until, output);
-	}
-	
-	public void escape(CharReader src, CharConsumer output) {
-		escape(src, -1, null, output);
-	}
-	
-	public int getEscapedLength(CharReader src, int length) {
-		return getEscapedLength(src, length, null);
-	}
-	
-	public int getEscapedLength(CharReader src, CharPredicate until) {
-		return getEscapedLength(src, -1, until);
-	}
-	
-	public int getEscapedLength(CharReader src) {
-		return getEscapedLength(src, -1, null);
-	}
-	
-	public char[] escape(CharReader src, int length, CharPredicate until) {
-		src.mark();
-		char[] result = new char[getEscapedLength(src, length, until)];
-		src.reset();
-		escape(src, length, until, CharConsumers.fillArray(result));
-		return result;
-	}
-	
-	public char[] escape(CharReader src, int length) {
-		return escape(src, length, (CharPredicate) null);
-	}
-	
-	public char[] escape(CharReader src, CharPredicate until) {
-		return escape(src, -1, until);
-	}
-	
-	public char[] escape(CharReader src) {
-		return escape(src, -1, (CharPredicate) null);
-	}
-	
-	public void unescape(CharReader src, int length, CharConsumer output) throws EscapeException {
-		unescape(src, length, null, output);
-	}
-	
-	public void unescape(CharReader src, CharPredicate until, CharConsumer output) throws EscapeException {
-		unescape(src, -1, until, output);
-	}
-	
-	public void unescape(CharReader src, CharConsumer output) throws EscapeException {
-		unescape(src, -1, null, output);
-	}
-	
-	public int getUnescapedLength(CharReader src, int length) {
-		return getUnescapedLength(src, length, null);
-	}
-	
-	public int getUnescapedLength(CharReader src, CharPredicate until) {
-		return getUnescapedLength(src, -1, until);
-	}
-	
-	public int getUnescapedLength(CharReader src) {
-		return getUnescapedLength(src, -1, null);
-	}
-	
-	public char[] unescape(CharReader src, int length, CharPredicate until) throws EscapeException {
-		src.mark();
-		char[] result = new char[getUnescapedLength(src, length, until)];
-		src.reset();
-		unescape(src, length, until, CharConsumers.fillArray(result));
-		return result;
-	}
-	
-	public char[] unescape(CharReader src, int length) throws EscapeException {
-		return unescape(src, length, (CharPredicate) null);
-	}
-	
-	public char[] unescape(CharReader src, CharPredicate until) throws EscapeException {
-		return unescape(src, -1, until);
-	}
-	
-	public char[] unescape(CharReader src) throws EscapeException {
-		return unescape(src, -1, (CharPredicate) null);
-	}
-
-	@Deprecated()
-	public char[] unescape(CharacterIterator src, char until) throws EscapeException {
-		int index = src.getIndex();
-		CharReader reader = CharReaders.wrap(src);
-		
-		char[] result = unescape(reader, -1, CharPredicate.forChar(until));
-		
-		src.setIndex(index + reader.getPosition());
-		return result;
-	}
-	
-	public String escape(String src) {
-		StringBuilder result = new StringBuilder(src.length());
-		escape(CharReaders.wrap(src), (CharConsumer) result::append);
-		return result.toString();
-	}
-	
-	public String unescape(String src) throws EscapeException {
-		StringBuilder result = new StringBuilder(src.length());
-		unescape(CharReaders.wrap(src), (CharConsumer) result::append);
-		return result.toString();
-	}
-	
-	/*
-	 * Misc
-	 */
-	
-	private static int hexValue(char c) throws EscapeException {
-		if (c <  '0') throw thisIsNotAHexDigit(c);
-		if (c <= '9') return c - '0';
-		if (c <  'A') throw thisIsNotAHexDigit(c);
-		if (c <= 'F') return c - 'A';
-		if (c <  'a') throw thisIsNotAHexDigit(c);
-		if (c <= 'f') return c - 'a';
-		if (c == CharReader.DONE) throw new EscapeException("Incomplete Unicode escape sequence at the end");
-		throw thisIsNotAHexDigit(c);
-	}
-	
-	private static EscapeException thisIsNotAHexDigit(char c) {
-		return new EscapeException("Invalid hex digit '" + c + "', expected [0-9A-Fa-f]");
-	}
-
-	protected static boolean isRegular(char c) {
-		return c >= ' ' && c <= '~';
-	}
-	
-	/*
-	 * Getters / setters
-	 */
-
-	public char getEscapeChar() {
-		return escapeChar;
-	}
-
-	public char getUnicodeEscapeChar() {
-		return unicodeEscapeChar;
-	}
-
-	public char[] getSafes() {
-		return safes;
-	}
-
-	public char[] getUnsafes() {
-		return unsafes;
-	}
-
-	public boolean isPreferUnicode() {
-		return preferUnicode;
-	}
-
-	public boolean isStrict() {
-		return strict;
-	}
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars;
+
+import java.text.CharacterIterator;
+
+import ru.windcorp.jputil.ArrayUtil;
+import ru.windcorp.jputil.chars.reader.CharReader;
+import ru.windcorp.jputil.chars.reader.CharReaders;
+
+public class Escaper {
+
+	public static class EscaperBuilder {
+		private char escapeChar = '\\';
+		private char unicodeEscapeChar = 'u';
+		private char[] safes = null;
+		private char[] unsafes = null;
+
+		private boolean preferUnicode = false;
+		private boolean strict = true;
+
+		public EscaperBuilder withEscapeChar(char escapeChar) {
+			this.escapeChar = escapeChar;
+			return this;
+		}
+
+		public EscaperBuilder withUnicodeEscapeChar(char unicodeEscapeChar) {
+			this.unicodeEscapeChar = unicodeEscapeChar;
+			return this;
+		}
+
+		public EscaperBuilder withChars(char[] safes, char[] unsafes) {
+			this.safes = safes;
+			this.unsafes = unsafes;
+			return this;
+		}
+
+		public EscaperBuilder withChars(String safes, String unsafes) {
+			this.safes = safes.toCharArray();
+			this.unsafes = unsafes.toCharArray();
+			return this;
+		}
+
+		public EscaperBuilder withChars(char[] chars) {
+			this.safes = this.unsafes = chars;
+			return this;
+		}
+
+		public EscaperBuilder withChars(String chars) {
+			this.safes = this.unsafes = chars.toCharArray();
+			return this;
+		}
+
+		public EscaperBuilder withSafes(char[] safes) {
+			this.safes = safes;
+			return this;
+		}
+
+		public EscaperBuilder withSafes(String safes) {
+			this.safes = safes.toCharArray();
+			return this;
+		}
+
+		public EscaperBuilder withUnsafes(char[] unsafes) {
+			this.unsafes = unsafes;
+			return this;
+		}
+
+		public EscaperBuilder withUnsafes(String unsafes) {
+			this.unsafes = unsafes.toCharArray();
+			return this;
+		}
+
+		public EscaperBuilder preferUnicode(boolean preferUnicode) {
+			this.preferUnicode = preferUnicode;
+			return this;
+		}
+
+		public EscaperBuilder strict(boolean strict) {
+			this.strict = strict;
+			return this;
+		}
+
+		public Escaper build() {
+			return new Escaper(escapeChar, unicodeEscapeChar, safes, unsafes, preferUnicode, strict);
+		}
+
+	}
+
+	public static final Escaper JAVA = new Escaper(
+		'\\',
+		'u',
+		"tbnrf'\"".toCharArray(),
+		"\t\b\n\r\f\'\"".toCharArray(),
+		true,
+		true
+	);
+
+	private final char escapeChar;
+	private final char unicodeEscapeChar;
+	private final char[] safes;
+	private final char[] unsafes;
+
+	private final boolean preferUnicode;
+	private final boolean strict;
+
+	protected Escaper(
+		char escapeChar,
+		char unicodeEscapeChar,
+		char[] safes,
+		char[] unsafes,
+		boolean preferUnicode,
+		boolean strict
+	) {
+		this.escapeChar = escapeChar;
+		this.unicodeEscapeChar = unicodeEscapeChar;
+		this.safes = safes;
+		this.unsafes = unsafes;
+		this.preferUnicode = preferUnicode;
+		this.strict = strict;
+
+		int duplicate;
+		if ((duplicate = ArrayUtil.hasDuplicates(safes)) != -1)
+			throw new IllegalArgumentException("Duplicate safe character '" + safes[duplicate] + "'");
+
+		if ((duplicate = ArrayUtil.hasDuplicates(unsafes)) != -1)
+			throw new IllegalArgumentException("Duplicate unsafe character '" + unsafes[duplicate] + "'");
+
+		for (char c : safes) {
+			if (c == escapeChar)
+				throw new IllegalArgumentException("Safe characters contain escape chatacter");
+			if (c == unicodeEscapeChar)
+				throw new IllegalArgumentException("Safe characters contain Unicode escape chatacter");
+		}
+
+		for (char c : unsafes) {
+			if (c == escapeChar)
+				throw new IllegalArgumentException(
+					"Unsafe characters contain escape chatacter (escape character is escaped automatically)"
+				);
+			if (c == unicodeEscapeChar)
+				throw new IllegalArgumentException("Unsafe characters contain Unicode escape chatacter");
+		}
+	}
+
+	public static EscaperBuilder create() {
+		return new EscaperBuilder();
+	}
+
+	/*
+	 * Logic - escape
+	 */
+
+	public void escape(CharReader src, int length, CharPredicate until, CharConsumer output) {
+		int end;
+		if (length < 0)
+			end = Integer.MAX_VALUE;
+		else
+			end = src.getPosition() + length;
+		while (
+			src.has() &&
+				src.getPosition() < end &&
+				(until == null || !until.test(src.current()))
+		)
+			escape(src.consume(), output);
+	}
+
+	public void escape(char c, CharConsumer output) {
+		if (c == escapeChar) {
+			output.accept(escapeChar);
+			output.accept(escapeChar);
+			return;
+		}
+
+		int index = ArrayUtil.firstIndexOf(unsafes, c);
+
+		if (index >= 0) {
+			output.accept(escapeChar);
+			output.accept(safes[index]);
+		} else {
+			if (preferUnicode && !isRegular(c)) {
+				escapeAsHex(c, output);
+			} else {
+				output.accept(c);
+			}
+		}
+	}
+
+	// SonarLint: Assignments should not be made from within sub-expressions
+	// (java:S1121)
+	// Seems self-evident enough
+	@SuppressWarnings("squid:S1121")
+
+	private void escapeAsHex(char c, CharConsumer output) {
+		output.accept(escapeChar);
+		output.accept(unicodeEscapeChar);
+		output.accept(StringUtil.hexDigit(c >>= (4 * 3)));
+		output.accept(StringUtil.hexDigit(c >>= (4 * 2)));
+		output.accept(StringUtil.hexDigit(c >>= (4 * 1)));
+		output.accept(StringUtil.hexDigit(c >> (4 * 0)));
+	}
+
+	public int getEscapedLength(CharReader src, int length, CharPredicate until) {
+		int end;
+		if (length < 0)
+			end = Integer.MAX_VALUE;
+		else
+			end = src.getPosition() + length;
+
+		int result = 0;
+
+		while (
+			src.has() &&
+				src.getPosition() < end &&
+				(until == null || !until.test(src.current()))
+		) {
+			result += getEscapedLength(src.consume());
+		}
+
+		return result;
+	}
+
+	public int getEscapedLength(char c) {
+		if (c == escapeChar || ArrayUtil.firstIndexOf(unsafes, c) >= 0)
+			return 2;
+		else {
+			if (preferUnicode && !isRegular(c))
+				return 6;
+			else
+				return 1;
+		}
+	}
+
+	/*
+	 * Logic - unescape
+	 */
+
+	public void unescape(CharReader src, int length, CharPredicate until, CharConsumer output) throws EscapeException {
+		int end;
+		if (length < 0)
+			end = Integer.MAX_VALUE;
+		else
+			end = src.getPosition() + length;
+		while (
+			src.has() &&
+				src.getPosition() < end &&
+				(until == null || !until.test(src.current()))
+		) {
+			output.accept(unescapeOneSequence(src));
+		}
+	}
+
+	public char unescapeOneSequence(CharReader src) throws EscapeException {
+		int resetPos = src.getPosition();
+		try {
+			if (src.current() == escapeChar) {
+				src.next();
+
+				if (src.isEnd())
+					throw new EscapeException("Incomplete escape sequence at the end");
+
+				if (src.current() == escapeChar) {
+					src.next();
+					return escapeChar;
+				}
+
+				if (src.current() == unicodeEscapeChar) {
+					src.next();
+					return (char) (hexValue(src.consume()) << (4 * 3) |
+						hexValue(src.consume()) << (4 * 2) |
+						hexValue(src.consume()) << (4 * 1) |
+						hexValue(src.consume()) << (4 * 0));
+				}
+
+				int index = ArrayUtil.firstIndexOf(safes, src.current());
+				if (index >= 0) {
+					src.next();
+					return unsafes[index];
+				}
+
+				if (strict)
+					throw new EscapeException("Unknown escape sequence \"" + escapeChar + src.current() + "\"");
+				else
+					return src.consume();
+			} else
+				return src.consume();
+		} catch (EscapeException | RuntimeException e) {
+			src.setPosition(resetPos);
+			throw e;
+		}
+	}
+
+	public int getUnescapedLength(CharReader src, int length, CharPredicate until) {
+		int end;
+		if (length < 0)
+			end = Integer.MAX_VALUE;
+		else
+			end = src.getPosition() + length;
+
+		int result = 0;
+
+		while (
+			src.has() &&
+				src.getPosition() < end &&
+				(until == null || !until.test(src.current()))
+		) {
+			skipOneSequence(src);
+			result++;
+		}
+
+		return result;
+	}
+
+	public void skipOneSequence(CharReader src) {
+		if (
+			src.current() == escapeChar
+				&&
+				src.next() == unicodeEscapeChar
+		) {
+			src.advance(4);
+		}
+		src.next();
+	}
+
+	/*
+	 * Utility
+	 */
+
+	public void escape(CharReader src, int length, CharConsumer output) {
+		escape(src, length, null, output);
+	}
+
+	public void escape(CharReader src, CharPredicate until, CharConsumer output) {
+		escape(src, -1, until, output);
+	}
+
+	public void escape(CharReader src, CharConsumer output) {
+		escape(src, -1, null, output);
+	}
+
+	public int getEscapedLength(CharReader src, int length) {
+		return getEscapedLength(src, length, null);
+	}
+
+	public int getEscapedLength(CharReader src, CharPredicate until) {
+		return getEscapedLength(src, -1, until);
+	}
+
+	public int getEscapedLength(CharReader src) {
+		return getEscapedLength(src, -1, null);
+	}
+
+	public char[] escape(CharReader src, int length, CharPredicate until) {
+		src.mark();
+		char[] result = new char[getEscapedLength(src, length, until)];
+		src.reset();
+		escape(src, length, until, CharConsumers.fillArray(result));
+		return result;
+	}
+
+	public char[] escape(CharReader src, int length) {
+		return escape(src, length, (CharPredicate) null);
+	}
+
+	public char[] escape(CharReader src, CharPredicate until) {
+		return escape(src, -1, until);
+	}
+
+	public char[] escape(CharReader src) {
+		return escape(src, -1, (CharPredicate) null);
+	}
+
+	public void unescape(CharReader src, int length, CharConsumer output) throws EscapeException {
+		unescape(src, length, null, output);
+	}
+
+	public void unescape(CharReader src, CharPredicate until, CharConsumer output) throws EscapeException {
+		unescape(src, -1, until, output);
+	}
+
+	public void unescape(CharReader src, CharConsumer output) throws EscapeException {
+		unescape(src, -1, null, output);
+	}
+
+	public int getUnescapedLength(CharReader src, int length) {
+		return getUnescapedLength(src, length, null);
+	}
+
+	public int getUnescapedLength(CharReader src, CharPredicate until) {
+		return getUnescapedLength(src, -1, until);
+	}
+
+	public int getUnescapedLength(CharReader src) {
+		return getUnescapedLength(src, -1, null);
+	}
+
+	public char[] unescape(CharReader src, int length, CharPredicate until) throws EscapeException {
+		src.mark();
+		char[] result = new char[getUnescapedLength(src, length, until)];
+		src.reset();
+		unescape(src, length, until, CharConsumers.fillArray(result));
+		return result;
+	}
+
+	public char[] unescape(CharReader src, int length) throws EscapeException {
+		return unescape(src, length, (CharPredicate) null);
+	}
+
+	public char[] unescape(CharReader src, CharPredicate until) throws EscapeException {
+		return unescape(src, -1, until);
+	}
+
+	public char[] unescape(CharReader src) throws EscapeException {
+		return unescape(src, -1, (CharPredicate) null);
+	}
+
+	@Deprecated()
+	public char[] unescape(CharacterIterator src, char until) throws EscapeException {
+		int index = src.getIndex();
+		CharReader reader = CharReaders.wrap(src);
+
+		char[] result = unescape(reader, -1, CharPredicate.forChar(until));
+
+		src.setIndex(index + reader.getPosition());
+		return result;
+	}
+
+	public String escape(String src) {
+		StringBuilder result = new StringBuilder(src.length());
+		escape(CharReaders.wrap(src), (CharConsumer) result::append);
+		return result.toString();
+	}
+
+	public String unescape(String src) throws EscapeException {
+		StringBuilder result = new StringBuilder(src.length());
+		unescape(CharReaders.wrap(src), (CharConsumer) result::append);
+		return result.toString();
+	}
+
+	/*
+	 * Misc
+	 */
+
+	private static int hexValue(char c) throws EscapeException {
+		if (c < '0')
+			throw thisIsNotAHexDigit(c);
+		if (c <= '9')
+			return c - '0';
+		if (c < 'A')
+			throw thisIsNotAHexDigit(c);
+		if (c <= 'F')
+			return c - 'A';
+		if (c < 'a')
+			throw thisIsNotAHexDigit(c);
+		if (c <= 'f')
+			return c - 'a';
+		if (c == CharReader.DONE)
+			throw new EscapeException("Incomplete Unicode escape sequence at the end");
+		throw thisIsNotAHexDigit(c);
+	}
+
+	private static EscapeException thisIsNotAHexDigit(char c) {
+		return new EscapeException("Invalid hex digit '" + c + "', expected [0-9A-Fa-f]");
+	}
+
+	protected static boolean isRegular(char c) {
+		return c >= ' ' && c <= '~';
+	}
+
+	/*
+	 * Getters / setters
+	 */
+
+	public char getEscapeChar() {
+		return escapeChar;
+	}
+
+	public char getUnicodeEscapeChar() {
+		return unicodeEscapeChar;
+	}
+
+	public char[] getSafes() {
+		return safes;
+	}
+
+	public char[] getUnsafes() {
+		return unsafes;
+	}
+
+	public boolean isPreferUnicode() {
+		return preferUnicode;
+	}
+
+	public boolean isStrict() {
+		return strict;
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/FancyCharacterIterator.java b/src/main/java/ru/windcorp/jputil/chars/FancyCharacterIterator.java
index b309786..183b672 100644
--- a/src/main/java/ru/windcorp/jputil/chars/FancyCharacterIterator.java
+++ b/src/main/java/ru/windcorp/jputil/chars/FancyCharacterIterator.java
@@ -1,17 +1,21 @@
-/* 
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- * 
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
  */
+ 
 package ru.windcorp.jputil.chars;
 
 import java.text.CharacterIterator;
@@ -26,77 +30,81 @@ public class FancyCharacterIterator implements CharacterIterator {
 		this.data = data;
 	}
 
-	@Override
+	@Override
 	public char first() {
 		return obj.first();
 	}
 
-	@Override
+	@Override
 	public char last() {
 		return obj.last();
 	}
 
-	@Override
+	@Override
 	public char setIndex(int p) {
 		return obj.setIndex(p);
 	}
 
-	@Override
+	@Override
 	public char current() {
 		return obj.current();
 	}
 
-	@Override
+	@Override
 	public char next() {
 		return obj.next();
 	}
 
-	@Override
+	@Override
 	public char previous() {
 		return obj.previous();
 	}
 
-	@Override
+	@Override
 	public int getBeginIndex() {
 		return obj.getBeginIndex();
 	}
 
-	@Override
+	@Override
 	public int getEndIndex() {
 		return obj.getEndIndex();
 	}
 
-	@Override
+	@Override
 	public int getIndex() {
 		return obj.getIndex();
 	}
-	
+
 	@Override
 	public String toString() {
 		StringBuilder sb = new StringBuilder("\"");
 		sb.append(data);
 		sb.append("\"\n ");
-		for (int i = 0; i < obj.getIndex(); ++i) sb.append(' ');
+		for (int i = 0; i < obj.getIndex(); ++i)
+			sb.append(' ');
 		sb.append("^ Here.");
 		return sb.toString();
 	}
-	
-//	@SuppressWarnings("all") Just STFU, this _is_ terrific
-	
-	// SonarLint: "clone" should not be overridden (java:S2975)
-	//   And I wouldn't have done that if only CharacterIterator had not required exception safety.
-	// SonarLint: "toString()" and "clone()" methods should not return null (java:S2225)
-	//   The clause is unreachable: CharacterArrayIterator implements Cloneable and superclass is Object.
-	@SuppressWarnings({"squid:S2975", "squid:S2225"})
-
-	@Override
-	public FancyCharacterIterator clone() {
-		try {
-			return (FancyCharacterIterator) super.clone();
-		} catch (CloneNotSupportedException cnse) {
-			// Impossible
-			return null;
-		}
-	}
 
-}
\ No newline at end of file
+//	@SuppressWarnings("all") Just STFU, this _is_ terrific
+
+	// SonarLint: "clone" should not be overridden (java:S2975)
+	// And I wouldn't have done that if only CharacterIterator had not required
+	// exception safety.
+	// SonarLint: "toString()" and "clone()" methods should not return null
+	// (java:S2225)
+	// The clause is unreachable: CharacterArrayIterator implements Cloneable
+	// and superclass is Object.
+	@SuppressWarnings({ "squid:S2975", "squid:S2225" })
+
+	@Override
+	public FancyCharacterIterator clone() {
+		try {
+			return (FancyCharacterIterator) super.clone();
+		} catch (CloneNotSupportedException cnse) {
+			// Impossible
+			return null;
+		}
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/IndentedStringBuilder.java b/src/main/java/ru/windcorp/jputil/chars/IndentedStringBuilder.java
index 2bfe3b6..92ba836 100644
--- a/src/main/java/ru/windcorp/jputil/chars/IndentedStringBuilder.java
+++ b/src/main/java/ru/windcorp/jputil/chars/IndentedStringBuilder.java
@@ -1,135 +1,138 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars;
-
-public class IndentedStringBuilder {
-
-	private final StringBuilder sb = new StringBuilder();
-	
-	private int indentLevel = 0;
-	private boolean indentApplied = false;
-	
-	private String[] indentCache = new String[16];
-	private String indent = "";
-	private final char[] indentFill;
-	
-	public IndentedStringBuilder(char[] indentFill) {
-		this.indentFill = indentFill;
-	}
-	
-	public IndentedStringBuilder(String indentFill) {
-		this(indentFill.toCharArray());
-	}
-	
-	public IndentedStringBuilder(char indentChar, int length) {
-		this(StringUtil.sequence(indentChar, length));
-	}
-	
-	public IndentedStringBuilder() {
-		this(new char[] {' '});
-	}
-	
-	@Override
-	public String toString() {
-		return sb.toString();
-	}
-
-	public int getIndentLevel() {
-		return indentLevel;
-	}
-
-	public void setIndentLevel(int level) {
-		this.indentLevel = level;
-		updateIndent();
-	}
-	
-	public char[] getIndentFill() {
-		return indentFill;
-	}
-
-	protected void updateIndent() {
-		if (indentLevel < indentCache.length) {
-			indent = indentCache[indentLevel];
-			if (indent != null) return;
-		}
-		
-		char[] fill = getIndentFill();
-		char[] array = new char[fill.length * getIndentLevel()];
-		for (int i = 0; i < array.length; i += fill.length)
-			System.arraycopy(fill, 0, array, i, fill.length);
-		indent = new String(array);
-		
-		if (indentLevel < indentCache.length) {
-			indentCache[indentLevel] = indent;
-		}
-	}
-	
-	public IndentedStringBuilder indent() {
-		setIndentLevel(getIndentLevel() + 1);
-		return this;
-	}
-	
-	public IndentedStringBuilder unindent() {
-		setIndentLevel(getIndentLevel() - 1);
-		return this;
-	}
-	
-	public IndentedStringBuilder append(Object x) {
-		if (x == null) {
-			appendRaw("null");
-			return this;
-		}
-		
-		String str = x.toString();
-		int newLines = StringUtil.count(str, '\n');
-		
-		if (newLines == 0) {
-			appendRaw(str);
-			return this;
-		}
-		
-		String[] lines = StringUtil.split(str, '\n', newLines + 1);
-		appendRaw(lines[0]);
-		
-		for (int i = 1; i < lines.length; ++i) {
-			newLine();
-			appendRaw(lines[i]);
-		}
-		
-		return this;
-	}
-	
-	public IndentedStringBuilder appendRaw(String str) {
-		if (str.isEmpty()) return this; // Do not append indent
-		
-		if (!indentApplied) {
-			sb.append(indent);
-			indentApplied = true;
-		}
-		
-		sb.append(str);
-		return this;
-	}
-	
-	public IndentedStringBuilder newLine() {
-		sb.append('\n');
-		indentApplied = false;
-		return this;
-	}
-	
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars;
+
+public class IndentedStringBuilder {
+
+	private final StringBuilder sb = new StringBuilder();
+
+	private int indentLevel = 0;
+	private boolean indentApplied = false;
+
+	private String[] indentCache = new String[16];
+	private String indent = "";
+	private final char[] indentFill;
+
+	public IndentedStringBuilder(char[] indentFill) {
+		this.indentFill = indentFill;
+	}
+
+	public IndentedStringBuilder(String indentFill) {
+		this(indentFill.toCharArray());
+	}
+
+	public IndentedStringBuilder(char indentChar, int length) {
+		this(StringUtil.sequence(indentChar, length));
+	}
+
+	public IndentedStringBuilder() {
+		this(new char[] { ' ' });
+	}
+
+	@Override
+	public String toString() {
+		return sb.toString();
+	}
+
+	public int getIndentLevel() {
+		return indentLevel;
+	}
+
+	public void setIndentLevel(int level) {
+		this.indentLevel = level;
+		updateIndent();
+	}
+
+	public char[] getIndentFill() {
+		return indentFill;
+	}
+
+	protected void updateIndent() {
+		if (indentLevel < indentCache.length) {
+			indent = indentCache[indentLevel];
+			if (indent != null)
+				return;
+		}
+
+		char[] fill = getIndentFill();
+		char[] array = new char[fill.length * getIndentLevel()];
+		for (int i = 0; i < array.length; i += fill.length)
+			System.arraycopy(fill, 0, array, i, fill.length);
+		indent = new String(array);
+
+		if (indentLevel < indentCache.length) {
+			indentCache[indentLevel] = indent;
+		}
+	}
+
+	public IndentedStringBuilder indent() {
+		setIndentLevel(getIndentLevel() + 1);
+		return this;
+	}
+
+	public IndentedStringBuilder unindent() {
+		setIndentLevel(getIndentLevel() - 1);
+		return this;
+	}
+
+	public IndentedStringBuilder append(Object x) {
+		if (x == null) {
+			appendRaw("null");
+			return this;
+		}
+
+		String str = x.toString();
+		int newLines = StringUtil.count(str, '\n');
+
+		if (newLines == 0) {
+			appendRaw(str);
+			return this;
+		}
+
+		String[] lines = StringUtil.split(str, '\n', newLines + 1);
+		appendRaw(lines[0]);
+
+		for (int i = 1; i < lines.length; ++i) {
+			newLine();
+			appendRaw(lines[i]);
+		}
+
+		return this;
+	}
+
+	public IndentedStringBuilder appendRaw(String str) {
+		if (str.isEmpty())
+			return this; // Do not append indent
+
+		if (!indentApplied) {
+			sb.append(indent);
+			indentApplied = true;
+		}
+
+		sb.append(str);
+		return this;
+	}
+
+	public IndentedStringBuilder newLine() {
+		sb.append('\n');
+		indentApplied = false;
+		return this;
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/StringUtil.java b/src/main/java/ru/windcorp/jputil/chars/StringUtil.java
index 0aaf7a6..f08018a 100644
--- a/src/main/java/ru/windcorp/jputil/chars/StringUtil.java
+++ b/src/main/java/ru/windcorp/jputil/chars/StringUtil.java
@@ -1,940 +1,1032 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.Objects;
-import java.util.function.IntFunction;
-
-import ru.windcorp.jputil.ArrayUtil;
-
-public class StringUtil {
-	
-	private StringUtil() {}
-	
-	private static final String NULL_PLACEHOLDER = "[null]";
-	private static final String EMPTY_PLACEHOLDER = "[empty]";
-	private static final String DEFAULT_SEPARATOR = "; ";
-	
-	public static  String arrayToString(
-			T[] array,
-			String separator,
-			String empty,
-			String nullPlaceholder,
-			String nullArray
-	) {
-		
-		if (separator == null) {
-			throw new IllegalArgumentException(new NullPointerException());
-		}
-		
-		if (array == null) {
-			return nullArray;
-		}
-		
-		if (array.length == 0) {
-			return empty;
-		}
-		
-		StringBuilder sb = new StringBuilder(array[0] == null ? nullPlaceholder : array[0].toString());
-		
-		for (int i = 1; i < array.length; ++i) {
-			sb.append(separator);
-			sb.append(array[i] == null ? nullPlaceholder : array[i].toString());
-		}
-		
-		return sb.toString();
-	}
-	
-	public static  String arrayToString(T[] array, String separator) {
-		return arrayToString(array, separator, EMPTY_PLACEHOLDER, NULL_PLACEHOLDER, "[null array]");
-	}
-	
-	public static  String arrayToString(T[] array) {
-		return arrayToString(array, DEFAULT_SEPARATOR);
-	}
-	
-	public static String iteratorToString(
-			Iterator> iterator,
-			String separator,
-			String empty,
-			String nullPlaceholder,
-			String nullIterator
-	) {
-		
-		if (separator == null) {
-			throw new IllegalArgumentException(new NullPointerException());
-		}
-		
-		if (iterator == null) {
-			return nullIterator;
-		}
-		
-		if (!iterator.hasNext()) {
-			return empty;
-		}
-		
-		Object obj = iterator.next();
-		StringBuilder sb = new StringBuilder(obj == null ? nullPlaceholder : obj.toString());
-		
-		while (iterator.hasNext()) {
-			obj = iterator.next();
-			sb.append(separator);
-			sb.append(obj == null ? nullPlaceholder : obj.toString());
-		}
-		
-		return sb.toString();
-	}
-	
-	public static String iteratorToString(Iterator> iterator, String separator) {
-		return iteratorToString(iterator, separator, EMPTY_PLACEHOLDER, NULL_PLACEHOLDER, "[null iterator]");
-	}
-	
-	public static String iteratorToString(Iterator> iterator) {
-		return iteratorToString(iterator, DEFAULT_SEPARATOR);
-	}
-	
-	public static String iterableToString(
-			Iterable> iterable,
-			String separator,
-			String empty,
-			String nullPlaceholder,
-			String nullIterable
-	) {
-		
-		if (separator == null) {
-			throw new IllegalArgumentException(new NullPointerException());
-		}
-		
-		if (iterable == null) {
-			return nullIterable;
-		}
-		
-		return iteratorToString(iterable.iterator(), separator, empty, nullPlaceholder, nullIterable);
-	}
-	
-	public static String iterableToString(Iterable> iterable, String separator) {
-		return iterableToString(iterable, separator, EMPTY_PLACEHOLDER, NULL_PLACEHOLDER, "[null iterable]");
-	}
-	
-	public static String iterableToString(Iterable> iterable) {
-		return iterableToString(iterable, DEFAULT_SEPARATOR);
-	}
-	
-	public static  String supplierToString(
-			IntFunction supplier,
-			int length,
-			String separator,
-			String empty,
-			String nullPlaceholder,
-			String nullSupplier
-	) {
-		
-		if (separator == null) throw new IllegalArgumentException(new NullPointerException());
-		if (supplier == null) return nullSupplier;
-		if (length == 0) return empty;
-		
-		if (length > 0) {
-			return supplierToStringExactly(
-					supplier,
-					length,
-					separator,
-					nullPlaceholder
-			);
-		} else {
-			return supplierToStringUntilNull(
-					supplier,
-					separator,
-					empty
-			);
-		}
-		
-	}
-
-	private static  String supplierToStringExactly(
-			IntFunction supplier,
-			int length,
-			String separator,
-			String nullPlaceholder
-	) {
-		T element = supplier.apply(0);
-		
-		StringBuilder sb = new StringBuilder(element == null ? nullPlaceholder : element.toString());
-		
-		for (int i = 1; i < length; ++i) {
-			sb.append(separator);
-			element = supplier.apply(i);
-			sb.append(element == null ? nullPlaceholder : element.toString());
-		}
-		
-		return sb.toString();
-	}
-	
-	private static  String supplierToStringUntilNull(
-			IntFunction supplier,
-			String separator,
-			String empty
-	) {
-		T element = supplier.apply(0);
-
-		if (element == null) {
-			return empty;
-		}
-		
-		StringBuilder sb = new StringBuilder(element.toString());
-		
-		int i = 0;
-		while ((element = supplier.apply(i++)) != null) {
-			sb.append(separator);
-			sb.append(element);
-		}
-		
-		return sb.toString();
-	}
-
-	public static String supplierToString(IntFunction> supplier, int length, String separator) {
-		return supplierToString(supplier, length, separator, EMPTY_PLACEHOLDER, NULL_PLACEHOLDER, "[null supplier]");
-	}
-	
-	public static String supplierToString(IntFunction> supplier, String separator) {
-		return supplierToString(supplier, -1, separator, EMPTY_PLACEHOLDER, NULL_PLACEHOLDER, "[null supplier]");
-	}
-	
-	public static String supplierToString(IntFunction> supplier, int length) {
-		return supplierToString(supplier, length, DEFAULT_SEPARATOR);
-	}
-	
-	public static String supplierToString(IntFunction> supplier) {
-		return supplierToString(supplier, -1, DEFAULT_SEPARATOR);
-	}
-	
-	public static byte[] toJavaByteArray(String str) {
-		char[] chars = str.toCharArray();
-		byte[] bytes = new byte[chars.length];
-		
-		for (int i = 0; i < bytes.length; ++i) {
-			bytes[i] = (byte) chars[i];
-		}
-		
-		return bytes;
-	}
-	
-	public static int count(String src, char target) {
-		int i = 0;
-		for (char c : src.toCharArray()) {
-			if (c == target) {
-				++i;
-			}
-		}
-		
-		return i;
-	}
-	
-	public static String[] split(String src, char separator) {
-		return split(src, separator, count(src, separator) + 1);
-	}
-	
-	public static String[] split(String src, char separator, int arrayLength) {
-		if (arrayLength < 0) throw illegalArrayLength(arrayLength);
-		else if (arrayLength == 0) return new String[0];
-		else if (arrayLength == 1) return new String[] { src };
-		
-		String[] result = new String[arrayLength];
-		
-		int resultIndex = 0;
-		StringBuilder sb = new StringBuilder();
-		for (char c : src.toCharArray()) {
-			if (c == separator && (resultIndex + 1) < arrayLength) {
-				result[resultIndex] = resetStringBuilder(sb);
-				++resultIndex;
-			} else {
-				sb.append(c);
-			}
-		}
-		
-		result[resultIndex] = sb.toString();
-		
-		return result;
-	}
-	
-	public static int count(String src, char... target) {
-		int i = 0;
-		for (char c : src.toCharArray()) {
-			for (char t : target) {
-				if (c == t) {
-					++i;
-					break;
-				}
-			}
-		}
-		
-		return i;
-	}
-	
-	public static String[] split(String src, char... separator) {
-		return split(src, count(src, separator) + 1, separator);
-	}
-	
-	public static String[] split(String src, int arrayLength, char... separator) {
-		if (arrayLength < 0) throw illegalArrayLength(arrayLength);
-		else if (arrayLength == 0) return new String[0];
-		else if (arrayLength == 1) return new String[] { src };
-		
-		String[] result = new String[arrayLength];
-		
-		int resultIndex = 0;
-		StringBuilder sb = new StringBuilder();
-		
-		charLoop:
-		for (char c : src.toCharArray()) {
-			if ((resultIndex + 1) < arrayLength) {
-				for (char h : separator) {
-					if (c == h) {
-						result[resultIndex] = resetStringBuilder(sb);
-						++resultIndex;
-						continue charLoop;
-					}
-				}
-			}
-			
-			sb.append(c);
-		}
-		
-		result[resultIndex] = sb.toString();
-		
-		return result;
-	}
-	
-	public static int count(String src, CharPredicate test) {
-		int i = 0;
-		for (char c : src.toCharArray()) {
-			if (test.test(c)) i++;
-		}
-		
-		return i;
-	}
-	
-	public static String[] split(String src, CharPredicate test) {
-		return split(src, count(src, test) + 1, test);
-	}
-	
-	public static String[] split(String src, int arrayLength, CharPredicate test) {
-		if (arrayLength < 0) throw illegalArrayLength(arrayLength);
-		else if (arrayLength == 0) return new String[0];
-		else if (arrayLength == 1) return new String[] { src };
-		
-		String[] result = new String[arrayLength];
-		
-		int resultIndex = 0;
-		StringBuilder sb = new StringBuilder();
-		
-		charLoop:
-		for (char c : src.toCharArray()) {
-			if (
-					(resultIndex + 1) < arrayLength
-					&&
-					test.test(c)
-			) {
-				result[resultIndex] = resetStringBuilder(sb);
-				++resultIndex;
-				continue charLoop;
-			}
-			
-			sb.append(c);
-		}
-		
-		result[resultIndex] = sb.toString();
-		
-		return result;
-	}
-	
-	/**
-	 * Splits {@code src} at index {@code at} discarding the character at that index.
-	 * 
-	 * Indices {@code 0} and {@code src.length() - 1} produce {@code str} excluding
-	 * the specified character and {@code ""}.
-	 * 
-	 * @param src the String to split
-	 * @param at index to split at
-	 * @throws IllegalArgumentException if the index is out of bounds for {@code src}
-	 * @return an array containing the substrings, in order of encounter in {@code src}.
-	 * Its length is always 2.
-	 */
-	public static String[] splitAt(String src, int at) {
-		Objects.requireNonNull(src, "src");
-		
-		if (at < 0) {
-			throw new StringIndexOutOfBoundsException(at);
-		} else if (at >= src.length()) {
-			throw new StringIndexOutOfBoundsException(at);
-		}
-		
-		if (at == 0) {
-			return new String[] {"", src.substring(1)};
-		} else if (at == src.length()) {
-			return new String[] {src.substring(0, src.length() - 1), ""};
-		}
-		
-		return new String[] {
-				src.substring(0, at),
-				src.substring(at + 1)
-		};
-	}
-	
-	/**
-	 * Splits {@code src} at indices {@code at} discarding characters at those indices.
-	 * 
-	 * Indices {@code 0} and {@code src.length() - 1} produce extra zero-length outputs.
-	 * Duplicate indices produce extra zero-length outputs.
-	 * 
-	 * Examples:
-	 * 
-	 * splitAt("a.b.c", new int[] {1, 3})    -> {"a", "b", "c"}
-	 * splitAt("a..b",  new int[] {1, 2})    -> {"a", "", "b"}
-	 * splitAt(".b.",   new int[] {0, 2})    -> {"", "b", ""}
-	 * splitAt("a.b",   new int[] {1, 1, 1}) -> {"a", "", "", "b"}
-	 * 
-	 * @param src the String to split
-	 * @param at indices to split at, in any order
-	 * @throws IllegalArgumentException if some index is out of bounds for {@code src}
-	 * @return an array containing the substrings, in order of encounter in {@code src}.
-	 * Its length is always {@code at.length + 1}.
-	 */
-	public static String[] splitAt(String src, int... at) {
-		Objects.requireNonNull(src, "src");
-		Objects.requireNonNull(at, "at");
-		
-		if (at.length == 0) return new String[] {src};
-		if (at.length == 1) return splitAt(src, at[0]);
-		
-		int[] indices; // Always sorted
-		
-		if (ArrayUtil.isSorted(at, true)) {
-			indices = at;
-		} else {
-			indices = at.clone();
-			Arrays.sort(indices);
-		}
-		
-		if (indices[0] < 0) {
-			throw new StringIndexOutOfBoundsException(indices[0]);
-		} else if (indices[indices.length - 1] >= src.length()) {
-			throw new StringIndexOutOfBoundsException(indices[indices.length - 1]);
-		}
-		
-		String[] result = new String[at.length + 1];
-		
-		int start = 0;
-		int resultIndex = 0;
-		for (int index : indices) {
-			int end = index;
-			
-			String substring;
-			
-			if (end <= start) {
-				// Duplicate or successive index
-				substring = "";
-			} else {
-				substring = src.substring(start, end);
-			}
-			
-			result[resultIndex] = substring;
-			resultIndex++;
-			start = end + 1;
-		}
-		
-		result[resultIndex] = src.substring(start);
-		
-		return result;
-	}
-	
-	private static IllegalArgumentException illegalArrayLength(int length) {
-		return new IllegalArgumentException("arrayLength must be non-negative (" + length + ")");
-	}
-	
-	public static String remove(String src, char... remove) {
-		char[] result = new char[src.length() - count(src, remove)];
-		
-		char current;
-		int resultIndex = 0;
-		
-		mainLoop:
-		for (int srcIndex = 0; srcIndex < src.length(); ++srcIndex) {
-			current = src.charAt(srcIndex);
-			
-			for (char c : remove) {
-				if (current == c) {
-					continue mainLoop;
-				}
-			}
-			
-			result[resultIndex++] = current;
-		}
-		
-		return new String(result);
-	}
-	
-	public static String resetStringBuilder(StringBuilder sb) {
-		String result = sb.toString();
-		sb.setLength(0);
-		sb.ensureCapacity(10);
-		return result;
-	}
-	
-	public static String readToString(InputStream is, Charset encoding, int bufferSize) throws IOException {
-		char[] buffer = new char[bufferSize];
-		StringBuilder result = new StringBuilder();
-		
-		Reader reader = new InputStreamReader(is, encoding);
-		while (true) {
-		    int readChars = reader.read(buffer, 0, buffer.length);
-		    
-		    if (readChars == -1) {
-		    	break;
-		    }
-		    
-		    result.append(buffer, 0, readChars);
-		}
-		
-		return result.toString();
-	}
-	
-	public static boolean equalsPart(char[] a, char[] b, int beginPos, int endPos) {
-		if (beginPos < 0) {
-			throw new IllegalArgumentException("beginPos must be non-negative (" + beginPos + ")");
-		}
-		
-		if (endPos < beginPos) {
-			throw new IllegalArgumentException("endPos must be greater than or equal to beginPos (endPos="
-					+ endPos + ", beginPos=" + beginPos + ")");
-		}
-		
-		if (endPos >= Math.min(a.length, b.length)) {
-			return false; // At least one of the arrays does not contain at least one of the required elements
-		}
-		
-		for (int i = beginPos; i < endPos; ++i) {
-			if (a[i] != b[i]) {
-				return false;
-			}
-		}
-		
-		return true;
-	}
-
-	// Java 8 is for pussies
-	public static char[] join(char[]... srcs) {
-		int tmp = 0;
-		for (int i = 0; i < srcs.length; ++i) {
-			tmp += srcs[i].length;
-		}
-		
-		char[] result = new char[tmp];
-		tmp = 0;
-		for (int i = 0; i < srcs.length; ++i) {
-			System.arraycopy(srcs[i], 0, result, tmp, srcs[i].length);
-			tmp += srcs[i].length;
-		}
-		
-		return result;
-	}
-	
-	/**
-	 * Finds and returns the index of the specified appearance of the specified character
-	 * in the given array. The search starts at index 0.
-	 * Examples:
-	 * 
-	 * 
-	 * src | 
-	 * target | 
-	 * skip | 
-	 * returns | 
-	 * 
-	 * a.b.c | '.' | 0 | 1 | 
-	 * a.b.c | '.' | 1 | 3 | 
-	 * a.b.c | '.' | 2 | -1 | 
-	 * a.b.c | 'd' | any | -1 | 
-	 * 
-	 * @param src - the array to search in.
-	 * @param target - the character to search for.
-	 * @param skip - the amount of target characters to be skipped.
-	 * @return The index of the skip+1th target character or -1, if none found.
-	 * @see StringUtil#indexFromEnd(char[], char, int)
-	 */
-	public static int indexFromBeginning(char[] src, char target, int skip) {
-		for (int i = 0; i < src.length; ++i) {
-			if (src[i] == target) {
-				if (skip == 0) {
-					return i;
-				}
-				
-				--skip;
-			}
-		}
-		return -1;
-	}
-	
-	/**
-	 * Finds and returns the index of the specified appearance of the specified character
-	 * in the given array. The search starts at index src.length - 1.
-	 * Examples:
-	 * 
-	 * 
-	 * src | 
-	 * target | 
-	 * skip | 
-	 * returns | 
-	 * 
-	 * a.b.c | '.' | 0 | 3 | 
-	 * a.b.c | '.' | 1 | 1 | 
-	 * a.b.c | '.' | 2 | -1 | 
-	 * a.b.c | 'd' | any | -1 | 
-	 * 
-	 * @param src - the array to search in.
-	 * @param target - the character to search for.
-	 * @param skip - the amount of target characters to be skipped.
-	 * @return The index of the skip+1th targetcharacter
-	 * from the end of the array or -1, if none found.
-	 * @see StringUtil#indexFromBeginning(char[], char, int)
-	 */
-	public static int indexFromEnd(char[] src, char target, int skip) {
-		for (int i = src.length - 1; i >= 0; --i) {
-			if (src[i] == target) {
-				if (skip == 0) {
-					return i;
-				}
-				
-				--skip;
-			}
-		}
-		
-		return -1;
-	}
-	
-	public static String padToLeft(String src, int length, char c) {
-		if (length <= 0) {
-			throw new IllegalArgumentException("length must be positive (" + length + ")");
-		}
-		
-		if (length <= src.length()) {
-			return src;
-		}
-		
-		char[] result = new char[length];
-		
-		int i = 0;
-		for (; i < src.length(); ++i) {
-			result[i] = src.charAt(i);
-		}
-		
-		for (; i < length; ++i) {
-			result[i] = c;
-		}
-		
-		return new String(result);
-	}
-	
-	public static String padToLeft(String src, int length) {
-		return padToLeft(src, length, ' ');
-	}
-	
-	public static String padToRight(String src, int length, char c) {
-		if (length <= 0) {
-			throw new IllegalArgumentException("length must be positive (" + length + ")");
-		}
-		
-		if (length <= src.length()) {
-			return src;
-		}
-		
-		char[] result = new char[length];
-		
-		int i = 0;
-		int srcLength = src.length();
-		
-		for (; i < length - srcLength; ++i) {
-			result[i] = c;
-		}
-		
-		for (; i < length; ++i) {
-			result[i] = src.charAt(i - (length - srcLength));
-		}
-		
-		return new String(result);
-	}
-	
-	public static String padToRight(String src, int length) {
-		return padToRight(src, length, ' ');
-	}
-	
-	public static String center(String src, int length) {
-        return center(src, length, ' ');
-    }
-
-    public static String center(String src, int length, char filler) {
-    	if (length <= 0) {
-			throw new IllegalArgumentException("length must be positive (" + length + ")");
-		}
-    	
-        if (src == null || length <= src.length()) {
-            return src;
-        }
-        
-        char[] result = new char[length];
-
-        int leftPaddingLength = (length - src.length()) / 2;
-        
-        Arrays.fill(result, 0, leftPaddingLength, filler);
-        
-        for (int i = 0; i < src.length(); ++i) {
-        	result[i + leftPaddingLength] = src.charAt(i);
-        }
-        
-        Arrays.fill(result, leftPaddingLength + src.length(), result.length, filler);
-        
-        return new String(result);
-    }
-	
-	public static int countWords(String src) {
-		int i = 0;
-		boolean isWord = false;
-		
-		for (char c : src.toCharArray()) {
-			if (Character.isWhitespace(c)) {
-				if (isWord) {
-					isWord = false;
-					i++;
-				}
-			} else {
-				isWord = true;
-			}
-		}
-		
-		if (isWord) {
-			i++;
-		}
-		
-		return i;
-	}
-	
-	public static String[] splitWords(String src) {
-		String[] result = new String[countWords(src)];
-		
-		int i = 0;
-		StringBuilder sb = new StringBuilder();
-		for (char c : src.toCharArray()) {
-			if (Character.isWhitespace(c)) {
-				if (sb.length() != 0) {
-					result[i++] = resetStringBuilder(sb);
-				}
-			} else {
-				sb.append(c);
-			}
-		}
-		
-		if (sb.length() != 0) {
-			result[i] = resetStringBuilder(sb);
-		}
-		
-		return result;
-	}
-	
-	public static char[] sequence(char c, int length) {
-		char[] result = new char[length];
-		Arrays.fill(result, c);
-		return result;
-	}
-	
-	public static String stripPrefix(String string, String prefix) {
-		if (prefix != null && string.startsWith(prefix)) {
-			return string.substring(prefix.length());
-		}
-		
-		return string;
-	}
-	
-	public static String stripSuffix(String string, String suffix) {
-		if (suffix != null && string.endsWith(suffix)) {
-			return string.substring(suffix.length());
-		}
-		
-		return string;
-	}
-	
-	@SafeVarargs
-	public static Collection allCombinations(Iterable... parts) {
-		StringBuilder sb = new StringBuilder();
-		Collection result = new ArrayList<>();
-		buildCombinations(sb, result, parts, 0);
-		return result;
-	}
-
-	private static void buildCombinations(StringBuilder sb, Collection result, Iterable[] parts,
-			int index) {
-		if (index >= parts.length) {
-			result.add(sb.toString());
-		} else {
-			int startLength = sb.length();
-			for (String part : parts[index]) {
-				sb.append(part);
-				buildCombinations(sb, result, parts, index + 1);
-				sb.setLength(startLength);
-			}
-		}
-	}
-	
-	@SafeVarargs
-	public static String[] allCombinations(String[]... parts) {
-		StringBuilder sb = new StringBuilder();
-		
-		int length = 1;
-		for (String[] array : parts) length *= array.length;
-		String[] result = new String[length];
-		
-		buildCombinations(sb, result, new int[] {0}, parts, 0);
-		return result;
-	}
-
-	private static void buildCombinations(StringBuilder sb, String[] result, int[] resultIndex, String[][] parts,
-			int index) {
-		if (index >= parts.length) {
-			result[resultIndex[0]++] = sb.toString();
-		} else {
-			int startLength = sb.length();
-			for (String part : parts[index]) {
-				sb.append(part);
-				buildCombinations(sb, result, resultIndex, parts, index + 1);
-				sb.setLength(startLength);
-			}
-		}
-	}
-	
-	public static String toUnsignedHexString(byte b) {
-		int unsigned = b;
-		if (b < 0) {
-			unsigned += 0x100;
-		}
-		
-		char[] chars = new char[2];
-		
-		chars[0] = Character.forDigit(unsigned >>> 4, 0x10);
-		chars[1] = Character.forDigit(unsigned & 0x0F, 0x10);
-		
-		return new String(chars);
-	}
-	
-	public static String toUnsignedHexString(byte[] bytes, String separator, int size) {
-		StringBuilder sb = new StringBuilder();
-		
-		for (int i = 0; i < bytes.length; ++i) {
-			sb.append(toUnsignedHexString(bytes[i]));
-			if (i < bytes.length - 1 && ((i + 1) % size == 0)) {
-				sb.append(separator);
-			}
-		}
-		
-		return sb.toString();
-	}
-	
-	public static String toUnsignedHexString(byte[] bytes) {
-		return toUnsignedHexString(bytes, ", ", 1);
-	}
-	
-	public static char[] toFullHex(byte x) {
-		return toFullHex(x, Byte.BYTES);
-	}
-		
-	public static char[] toFullHex(short x) {
-		return toFullHex(x, Short.BYTES);
-	}
-	
-	public static char[] toFullHex(int x) {
-		return toFullHex(x, Integer.BYTES);
-	}
-	
-	public static char[] toFullHex(long x) {
-		return toFullHex(x, Long.BYTES);
-	}
-	
-	private static char[] toFullHex(long x, int bytes) {
-		final int digits = bytes * 2;
-		
-		char[] result = new char[digits + 2];
-		result[0] = '0';
-		result[1] = 'x';
-		
-		for (int digit = 0; digit < digits; ++digit) {
-			result[(digits - digit - 1) + 2] =
-					hexDigit(x, digit);
-		}
-		
-		return result;
-	}
-	
-	private static char hexDigit(long value, int digit) {
-		return hexDigit(
-				(int) (value >>> (4 * digit))
-				& 0xF
-		);
-	}
-	
-	public static char hexDigit(int value) {
-		if (value < 0xA) return (char) ('0' + value);
-		else             return (char) ('A' - 0xA + value);
-	}
-	
-	public static String replaceAll(String source, String substring, String replacement) {
-		Objects.requireNonNull(source, "source");
-		Objects.requireNonNull(substring, "substring");
-		
-		if (substring.isEmpty()) {
-			throw new IllegalArgumentException("substring is empty");
-		}
-
-		if (!source.contains(substring)) { // also passes if source is empty
-			return source;
-		}
-		
-		if (substring.equals(replacement)) { // null-safe
-			return source;
-		}
-		
-		StringBuilder sb = new StringBuilder(2 * source.length());
-		
-		for (int i = 0; i < source.length() - substring.length() + 1; ++i) {
-			if (source.startsWith(substring, i)) {
-				if (replacement != null) {
-					sb.append(replacement);
-				}
-			} else {
-				sb.append(source.charAt(i));
-			}
-		}
-		
-		return sb.toString();
-	}
-	
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Objects;
+import java.util.function.IntFunction;
+
+import ru.windcorp.jputil.ArrayUtil;
+
+public class StringUtil {
+
+	private StringUtil() {
+	}
+
+	private static final String NULL_PLACEHOLDER = "[null]";
+	private static final String EMPTY_PLACEHOLDER = "[empty]";
+	private static final String DEFAULT_SEPARATOR = "; ";
+
+	public static  String arrayToString(
+		T[] array,
+		String separator,
+		String empty,
+		String nullPlaceholder,
+		String nullArray
+	) {
+
+		if (separator == null) {
+			throw new IllegalArgumentException(new NullPointerException());
+		}
+
+		if (array == null) {
+			return nullArray;
+		}
+
+		if (array.length == 0) {
+			return empty;
+		}
+
+		StringBuilder sb = new StringBuilder(array[0] == null ? nullPlaceholder : array[0].toString());
+
+		for (int i = 1; i < array.length; ++i) {
+			sb.append(separator);
+			sb.append(array[i] == null ? nullPlaceholder : array[i].toString());
+		}
+
+		return sb.toString();
+	}
+
+	public static  String arrayToString(T[] array, String separator) {
+		return arrayToString(array, separator, EMPTY_PLACEHOLDER, NULL_PLACEHOLDER, "[null array]");
+	}
+
+	public static  String arrayToString(T[] array) {
+		return arrayToString(array, DEFAULT_SEPARATOR);
+	}
+
+	public static String iteratorToString(
+		Iterator> iterator,
+		String separator,
+		String empty,
+		String nullPlaceholder,
+		String nullIterator
+	) {
+
+		if (separator == null) {
+			throw new IllegalArgumentException(new NullPointerException());
+		}
+
+		if (iterator == null) {
+			return nullIterator;
+		}
+
+		if (!iterator.hasNext()) {
+			return empty;
+		}
+
+		Object obj = iterator.next();
+		StringBuilder sb = new StringBuilder(obj == null ? nullPlaceholder : obj.toString());
+
+		while (iterator.hasNext()) {
+			obj = iterator.next();
+			sb.append(separator);
+			sb.append(obj == null ? nullPlaceholder : obj.toString());
+		}
+
+		return sb.toString();
+	}
+
+	public static String iteratorToString(Iterator> iterator, String separator) {
+		return iteratorToString(iterator, separator, EMPTY_PLACEHOLDER, NULL_PLACEHOLDER, "[null iterator]");
+	}
+
+	public static String iteratorToString(Iterator> iterator) {
+		return iteratorToString(iterator, DEFAULT_SEPARATOR);
+	}
+
+	public static String iterableToString(
+		Iterable> iterable,
+		String separator,
+		String empty,
+		String nullPlaceholder,
+		String nullIterable
+	) {
+
+		if (separator == null) {
+			throw new IllegalArgumentException(new NullPointerException());
+		}
+
+		if (iterable == null) {
+			return nullIterable;
+		}
+
+		return iteratorToString(iterable.iterator(), separator, empty, nullPlaceholder, nullIterable);
+	}
+
+	public static String iterableToString(Iterable> iterable, String separator) {
+		return iterableToString(iterable, separator, EMPTY_PLACEHOLDER, NULL_PLACEHOLDER, "[null iterable]");
+	}
+
+	public static String iterableToString(Iterable> iterable) {
+		return iterableToString(iterable, DEFAULT_SEPARATOR);
+	}
+
+	public static  String supplierToString(
+		IntFunction supplier,
+		int length,
+		String separator,
+		String empty,
+		String nullPlaceholder,
+		String nullSupplier
+	) {
+
+		if (separator == null)
+			throw new IllegalArgumentException(new NullPointerException());
+		if (supplier == null)
+			return nullSupplier;
+		if (length == 0)
+			return empty;
+
+		if (length > 0) {
+			return supplierToStringExactly(
+				supplier,
+				length,
+				separator,
+				nullPlaceholder
+			);
+		} else {
+			return supplierToStringUntilNull(
+				supplier,
+				separator,
+				empty
+			);
+		}
+
+	}
+
+	private static  String supplierToStringExactly(
+		IntFunction supplier,
+		int length,
+		String separator,
+		String nullPlaceholder
+	) {
+		T element = supplier.apply(0);
+
+		StringBuilder sb = new StringBuilder(element == null ? nullPlaceholder : element.toString());
+
+		for (int i = 1; i < length; ++i) {
+			sb.append(separator);
+			element = supplier.apply(i);
+			sb.append(element == null ? nullPlaceholder : element.toString());
+		}
+
+		return sb.toString();
+	}
+
+	private static  String supplierToStringUntilNull(
+		IntFunction supplier,
+		String separator,
+		String empty
+	) {
+		T element = supplier.apply(0);
+
+		if (element == null) {
+			return empty;
+		}
+
+		StringBuilder sb = new StringBuilder(element.toString());
+
+		int i = 0;
+		while ((element = supplier.apply(i++)) != null) {
+			sb.append(separator);
+			sb.append(element);
+		}
+
+		return sb.toString();
+	}
+
+	public static String supplierToString(IntFunction> supplier, int length, String separator) {
+		return supplierToString(supplier, length, separator, EMPTY_PLACEHOLDER, NULL_PLACEHOLDER, "[null supplier]");
+	}
+
+	public static String supplierToString(IntFunction> supplier, String separator) {
+		return supplierToString(supplier, -1, separator, EMPTY_PLACEHOLDER, NULL_PLACEHOLDER, "[null supplier]");
+	}
+
+	public static String supplierToString(IntFunction> supplier, int length) {
+		return supplierToString(supplier, length, DEFAULT_SEPARATOR);
+	}
+
+	public static String supplierToString(IntFunction> supplier) {
+		return supplierToString(supplier, -1, DEFAULT_SEPARATOR);
+	}
+
+	public static byte[] toJavaByteArray(String str) {
+		char[] chars = str.toCharArray();
+		byte[] bytes = new byte[chars.length];
+
+		for (int i = 0; i < bytes.length; ++i) {
+			bytes[i] = (byte) chars[i];
+		}
+
+		return bytes;
+	}
+
+	public static int count(String src, char target) {
+		int i = 0;
+		for (char c : src.toCharArray()) {
+			if (c == target) {
+				++i;
+			}
+		}
+
+		return i;
+	}
+
+	public static String[] split(String src, char separator) {
+		return split(src, separator, count(src, separator) + 1);
+	}
+
+	public static String[] split(String src, char separator, int arrayLength) {
+		if (arrayLength < 0)
+			throw illegalArrayLength(arrayLength);
+		else if (arrayLength == 0)
+			return new String[0];
+		else if (arrayLength == 1)
+			return new String[] { src };
+
+		String[] result = new String[arrayLength];
+
+		int resultIndex = 0;
+		StringBuilder sb = new StringBuilder();
+		for (char c : src.toCharArray()) {
+			if (c == separator && (resultIndex + 1) < arrayLength) {
+				result[resultIndex] = resetStringBuilder(sb);
+				++resultIndex;
+			} else {
+				sb.append(c);
+			}
+		}
+
+		result[resultIndex] = sb.toString();
+
+		return result;
+	}
+
+	public static int count(String src, char... target) {
+		int i = 0;
+		for (char c : src.toCharArray()) {
+			for (char t : target) {
+				if (c == t) {
+					++i;
+					break;
+				}
+			}
+		}
+
+		return i;
+	}
+
+	public static String[] split(String src, char... separator) {
+		return split(src, count(src, separator) + 1, separator);
+	}
+
+	public static String[] split(String src, int arrayLength, char... separator) {
+		if (arrayLength < 0)
+			throw illegalArrayLength(arrayLength);
+		else if (arrayLength == 0)
+			return new String[0];
+		else if (arrayLength == 1)
+			return new String[] { src };
+
+		String[] result = new String[arrayLength];
+
+		int resultIndex = 0;
+		StringBuilder sb = new StringBuilder();
+
+		charLoop: for (char c : src.toCharArray()) {
+			if ((resultIndex + 1) < arrayLength) {
+				for (char h : separator) {
+					if (c == h) {
+						result[resultIndex] = resetStringBuilder(sb);
+						++resultIndex;
+						continue charLoop;
+					}
+				}
+			}
+
+			sb.append(c);
+		}
+
+		result[resultIndex] = sb.toString();
+
+		return result;
+	}
+
+	public static int count(String src, CharPredicate test) {
+		int i = 0;
+		for (char c : src.toCharArray()) {
+			if (test.test(c))
+				i++;
+		}
+
+		return i;
+	}
+
+	public static String[] split(String src, CharPredicate test) {
+		return split(src, count(src, test) + 1, test);
+	}
+
+	public static String[] split(String src, int arrayLength, CharPredicate test) {
+		if (arrayLength < 0)
+			throw illegalArrayLength(arrayLength);
+		else if (arrayLength == 0)
+			return new String[0];
+		else if (arrayLength == 1)
+			return new String[] { src };
+
+		String[] result = new String[arrayLength];
+
+		int resultIndex = 0;
+		StringBuilder sb = new StringBuilder();
+
+		charLoop: for (char c : src.toCharArray()) {
+			if (
+				(resultIndex + 1) < arrayLength
+					&&
+					test.test(c)
+			) {
+				result[resultIndex] = resetStringBuilder(sb);
+				++resultIndex;
+				continue charLoop;
+			}
+
+			sb.append(c);
+		}
+
+		result[resultIndex] = sb.toString();
+
+		return result;
+	}
+
+	/**
+	 * Splits {@code src} at index {@code at} discarding the character at that
+	 * index.
+	 * 
+	 * Indices {@code 0} and {@code src.length() - 1} produce {@code str}
+	 * excluding
+	 * the specified character and {@code ""}.
+	 * 
+	 * 
+	 * @param src the String to split
+	 * @param at  index to split at
+	 * @throws IllegalArgumentException if the index is out of bounds for
+	 *                                  {@code src}
+	 * @return an array containing the substrings, in order of encounter in
+	 *         {@code src}.
+	 *         Its length is always 2.
+	 */
+	public static String[] splitAt(String src, int at) {
+		Objects.requireNonNull(src, "src");
+
+		if (at < 0) {
+			throw new StringIndexOutOfBoundsException(at);
+		} else if (at >= src.length()) {
+			throw new StringIndexOutOfBoundsException(at);
+		}
+
+		if (at == 0) {
+			return new String[] { "", src.substring(1) };
+		} else if (at == src.length()) {
+			return new String[] { src.substring(0, src.length() - 1), "" };
+		}
+
+		return new String[] {
+			src.substring(0, at),
+			src.substring(at + 1)
+		};
+	}
+
+	/**
+	 * Splits {@code src} at indices {@code at} discarding characters at those
+	 * indices.
+	 * 
+	 * Indices {@code 0} and {@code src.length() - 1} produce extra zero-length
+	 * outputs.
+	 * Duplicate indices produce extra zero-length outputs.
+	 * 
+	 * Examples:
+	 * 
+	 * 
+	 * splitAt("a.b.c", 1, 3)    -> {"a", "b", "c"}
+	 * splitAt("a..b",  1, 2)    -> {"a", "", "b"}
+	 * splitAt(".b.",   0, 2)    -> {"", "b", ""}
+	 * splitAt("a.b",   1, 1, 1) -> {"a", "", "", "b"}
+	 * 
+	 * 
+	 * @param src the String to split
+	 * @param at  indices to split at, in any order
+	 * @throws IllegalArgumentException if some index is out of bounds for
+	 *                                  {@code src}
+	 * @return an array containing the substrings, in order of encounter in
+	 *         {@code src}.
+	 *         Its length is always {@code at.length + 1}.
+	 */
+	public static String[] splitAt(String src, int... at) {
+		Objects.requireNonNull(src, "src");
+		Objects.requireNonNull(at, "at");
+
+		if (at.length == 0)
+			return new String[] { src };
+		if (at.length == 1)
+			return splitAt(src, at[0]);
+
+		int[] indices; // Always sorted
+
+		if (ArrayUtil.isSorted(at, true)) {
+			indices = at;
+		} else {
+			indices = at.clone();
+			Arrays.sort(indices);
+		}
+
+		if (indices[0] < 0) {
+			throw new StringIndexOutOfBoundsException(indices[0]);
+		} else if (indices[indices.length - 1] >= src.length()) {
+			throw new StringIndexOutOfBoundsException(indices[indices.length - 1]);
+		}
+
+		String[] result = new String[at.length + 1];
+
+		int start = 0;
+		int resultIndex = 0;
+		for (int index : indices) {
+			int end = index;
+
+			String substring;
+
+			if (end <= start) {
+				// Duplicate or successive index
+				substring = "";
+			} else {
+				substring = src.substring(start, end);
+			}
+
+			result[resultIndex] = substring;
+			resultIndex++;
+			start = end + 1;
+		}
+
+		result[resultIndex] = src.substring(start);
+
+		return result;
+	}
+
+	private static IllegalArgumentException illegalArrayLength(int length) {
+		return new IllegalArgumentException("arrayLength must be non-negative (" + length + ")");
+	}
+
+	public static String remove(String src, char... remove) {
+		char[] result = new char[src.length() - count(src, remove)];
+
+		char current;
+		int resultIndex = 0;
+
+		mainLoop: for (int srcIndex = 0; srcIndex < src.length(); ++srcIndex) {
+			current = src.charAt(srcIndex);
+
+			for (char c : remove) {
+				if (current == c) {
+					continue mainLoop;
+				}
+			}
+
+			result[resultIndex++] = current;
+		}
+
+		return new String(result);
+	}
+
+	public static String resetStringBuilder(StringBuilder sb) {
+		String result = sb.toString();
+		sb.setLength(0);
+		sb.ensureCapacity(10);
+		return result;
+	}
+
+	public static String readToString(InputStream is, Charset encoding, int bufferSize) throws IOException {
+		char[] buffer = new char[bufferSize];
+		StringBuilder result = new StringBuilder();
+
+		Reader reader = new InputStreamReader(is, encoding);
+		while (true) {
+			int readChars = reader.read(buffer, 0, buffer.length);
+
+			if (readChars == -1) {
+				break;
+			}
+
+			result.append(buffer, 0, readChars);
+		}
+
+		return result.toString();
+	}
+
+	public static boolean equalsPart(char[] a, char[] b, int beginPos, int endPos) {
+		if (beginPos < 0) {
+			throw new IllegalArgumentException("beginPos must be non-negative (" + beginPos + ")");
+		}
+
+		if (endPos < beginPos) {
+			throw new IllegalArgumentException(
+				"endPos must be greater than or equal to beginPos (endPos="
+					+ endPos + ", beginPos=" + beginPos + ")"
+			);
+		}
+
+		if (endPos >= Math.min(a.length, b.length)) {
+			return false; // At least one of the arrays does not contain at
+							// least one of the required elements
+		}
+
+		for (int i = beginPos; i < endPos; ++i) {
+			if (a[i] != b[i]) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	// Java 8 is for pussies
+	public static char[] join(char[]... srcs) {
+		int tmp = 0;
+		for (int i = 0; i < srcs.length; ++i) {
+			tmp += srcs[i].length;
+		}
+
+		char[] result = new char[tmp];
+		tmp = 0;
+		for (int i = 0; i < srcs.length; ++i) {
+			System.arraycopy(srcs[i], 0, result, tmp, srcs[i].length);
+			tmp += srcs[i].length;
+		}
+
+		return result;
+	}
+
+	/**
+	 * Finds and returns the index of the specified appearance of the specified
+	 * character
+	 * in the given array. The search starts at index 0.
+	 * 
+	 * Examples:
+	 * 
+	 * 
+	 * 
+	 * src | 
+	 * target | 
+	 * skip | 
+	 * returns | 
+	 * 
+	 * 
+	 * a.b.c | 
+	 * '.' | 
+	 * 0 | 
+	 * 1 | 
+	 * 
+	 * 
+	 * a.b.c | 
+	 * '.' | 
+	 * 1 | 
+	 * 3 | 
+	 * 
+	 * 
+	 * a.b.c | 
+	 * '.' | 
+	 * 2 | 
+	 * -1 | 
+	 * 
+	 * 
+	 * a.b.c | 
+	 * 'd' | 
+	 * any | 
+	 * -1 | 
+	 * 
+	 * 
+	 * 
+	 * @param src    - the array to search in.
+	 * @param target - the character to search for.
+	 * @param skip   - the amount of target characters to be
+	 *               skipped.
+	 * @return The index of the skip+1th target
+	 *         character or -1, if none found.
+	 * @see StringUtil#indexFromEnd(char[], char, int)
+	 */
+	public static int indexFromBeginning(char[] src, char target, int skip) {
+		for (int i = 0; i < src.length; ++i) {
+			if (src[i] == target) {
+				if (skip == 0) {
+					return i;
+				}
+
+				--skip;
+			}
+		}
+		return -1;
+	}
+
+	/**
+	 * Finds and returns the index of the specified appearance of the specified
+	 * character
+	 * in the given array. The search starts at index
+	 * src.length - 1.
+	 * 
+	 * Examples:
+	 * 
+	 * 
+	 * 
+	 * src | 
+	 * target | 
+	 * skip | 
+	 * returns | 
+	 * 
+	 * 
+	 * a.b.c | 
+	 * '.' | 
+	 * 0 | 
+	 * 3 | 
+	 * 
+	 * 
+	 * a.b.c | 
+	 * '.' | 
+	 * 1 | 
+	 * 1 | 
+	 * 
+	 * 
+	 * a.b.c | 
+	 * '.' | 
+	 * 2 | 
+	 * -1 | 
+	 * 
+	 * 
+	 * a.b.c | 
+	 * 'd' | 
+	 * any | 
+	 * -1 | 
+	 * 
+	 * 
+	 * 
+	 * @param src    - the array to search in.
+	 * @param target - the character to search for.
+	 * @param skip   - the amount of target characters to be
+	 *               skipped.
+	 * @return The index of the skip+1th
+	 *         targetcharacter
+	 *         from the end of the array or -1, if none found.
+	 * @see StringUtil#indexFromBeginning(char[], char, int)
+	 */
+	public static int indexFromEnd(char[] src, char target, int skip) {
+		for (int i = src.length - 1; i >= 0; --i) {
+			if (src[i] == target) {
+				if (skip == 0) {
+					return i;
+				}
+
+				--skip;
+			}
+		}
+
+		return -1;
+	}
+
+	public static String padToLeft(String src, int length, char c) {
+		if (length <= 0) {
+			throw new IllegalArgumentException("length must be positive (" + length + ")");
+		}
+
+		if (length <= src.length()) {
+			return src;
+		}
+
+		char[] result = new char[length];
+
+		int i = 0;
+		for (; i < src.length(); ++i) {
+			result[i] = src.charAt(i);
+		}
+
+		for (; i < length; ++i) {
+			result[i] = c;
+		}
+
+		return new String(result);
+	}
+
+	public static String padToLeft(String src, int length) {
+		return padToLeft(src, length, ' ');
+	}
+
+	public static String padToRight(String src, int length, char c) {
+		if (length <= 0) {
+			throw new IllegalArgumentException("length must be positive (" + length + ")");
+		}
+
+		if (length <= src.length()) {
+			return src;
+		}
+
+		char[] result = new char[length];
+
+		int i = 0;
+		int srcLength = src.length();
+
+		for (; i < length - srcLength; ++i) {
+			result[i] = c;
+		}
+
+		for (; i < length; ++i) {
+			result[i] = src.charAt(i - (length - srcLength));
+		}
+
+		return new String(result);
+	}
+
+	public static String padToRight(String src, int length) {
+		return padToRight(src, length, ' ');
+	}
+
+	public static String center(String src, int length) {
+		return center(src, length, ' ');
+	}
+
+	public static String center(String src, int length, char filler) {
+		if (length <= 0) {
+			throw new IllegalArgumentException("length must be positive (" + length + ")");
+		}
+
+		if (src == null || length <= src.length()) {
+			return src;
+		}
+
+		char[] result = new char[length];
+
+		int leftPaddingLength = (length - src.length()) / 2;
+
+		Arrays.fill(result, 0, leftPaddingLength, filler);
+
+		for (int i = 0; i < src.length(); ++i) {
+			result[i + leftPaddingLength] = src.charAt(i);
+		}
+
+		Arrays.fill(result, leftPaddingLength + src.length(), result.length, filler);
+
+		return new String(result);
+	}
+
+	public static int countWords(String src) {
+		int i = 0;
+		boolean isWord = false;
+
+		for (char c : src.toCharArray()) {
+			if (Character.isWhitespace(c)) {
+				if (isWord) {
+					isWord = false;
+					i++;
+				}
+			} else {
+				isWord = true;
+			}
+		}
+
+		if (isWord) {
+			i++;
+		}
+
+		return i;
+	}
+
+	public static String[] splitWords(String src) {
+		String[] result = new String[countWords(src)];
+
+		int i = 0;
+		StringBuilder sb = new StringBuilder();
+		for (char c : src.toCharArray()) {
+			if (Character.isWhitespace(c)) {
+				if (sb.length() != 0) {
+					result[i++] = resetStringBuilder(sb);
+				}
+			} else {
+				sb.append(c);
+			}
+		}
+
+		if (sb.length() != 0) {
+			result[i] = resetStringBuilder(sb);
+		}
+
+		return result;
+	}
+
+	public static char[] sequence(char c, int length) {
+		char[] result = new char[length];
+		Arrays.fill(result, c);
+		return result;
+	}
+
+	public static String stripPrefix(String string, String prefix) {
+		if (prefix != null && string.startsWith(prefix)) {
+			return string.substring(prefix.length());
+		}
+
+		return string;
+	}
+
+	public static String stripSuffix(String string, String suffix) {
+		if (suffix != null && string.endsWith(suffix)) {
+			return string.substring(suffix.length());
+		}
+
+		return string;
+	}
+
+	@SafeVarargs
+	public static Collection allCombinations(Iterable... parts) {
+		StringBuilder sb = new StringBuilder();
+		Collection result = new ArrayList<>();
+		buildCombinations(sb, result, parts, 0);
+		return result;
+	}
+
+	private static void buildCombinations(
+		StringBuilder sb,
+		Collection result,
+		Iterable[] parts,
+		int index
+	) {
+		if (index >= parts.length) {
+			result.add(sb.toString());
+		} else {
+			int startLength = sb.length();
+			for (String part : parts[index]) {
+				sb.append(part);
+				buildCombinations(sb, result, parts, index + 1);
+				sb.setLength(startLength);
+			}
+		}
+	}
+
+	@SafeVarargs
+	public static String[] allCombinations(String[]... parts) {
+		StringBuilder sb = new StringBuilder();
+
+		int length = 1;
+		for (String[] array : parts)
+			length *= array.length;
+		String[] result = new String[length];
+
+		buildCombinations(sb, result, new int[] { 0 }, parts, 0);
+		return result;
+	}
+
+	private static void buildCombinations(
+		StringBuilder sb,
+		String[] result,
+		int[] resultIndex,
+		String[][] parts,
+		int index
+	) {
+		if (index >= parts.length) {
+			result[resultIndex[0]++] = sb.toString();
+		} else {
+			int startLength = sb.length();
+			for (String part : parts[index]) {
+				sb.append(part);
+				buildCombinations(sb, result, resultIndex, parts, index + 1);
+				sb.setLength(startLength);
+			}
+		}
+	}
+
+	public static String toUnsignedHexString(byte b) {
+		int unsigned = b;
+		if (b < 0) {
+			unsigned += 0x100;
+		}
+
+		char[] chars = new char[2];
+
+		chars[0] = Character.forDigit(unsigned >>> 4, 0x10);
+		chars[1] = Character.forDigit(unsigned & 0x0F, 0x10);
+
+		return new String(chars);
+	}
+
+	public static String toUnsignedHexString(byte[] bytes, String separator, int size) {
+		StringBuilder sb = new StringBuilder();
+
+		for (int i = 0; i < bytes.length; ++i) {
+			sb.append(toUnsignedHexString(bytes[i]));
+			if (i < bytes.length - 1 && ((i + 1) % size == 0)) {
+				sb.append(separator);
+			}
+		}
+
+		return sb.toString();
+	}
+
+	public static String toUnsignedHexString(byte[] bytes) {
+		return toUnsignedHexString(bytes, ", ", 1);
+	}
+
+	public static char[] toFullHex(byte x) {
+		return toFullHex(x, Byte.BYTES);
+	}
+
+	public static char[] toFullHex(short x) {
+		return toFullHex(x, Short.BYTES);
+	}
+
+	public static char[] toFullHex(int x) {
+		return toFullHex(x, Integer.BYTES);
+	}
+
+	public static char[] toFullHex(long x) {
+		return toFullHex(x, Long.BYTES);
+	}
+
+	private static char[] toFullHex(long x, int bytes) {
+		final int digits = bytes * 2;
+
+		char[] result = new char[digits + 2];
+		result[0] = '0';
+		result[1] = 'x';
+
+		for (int digit = 0; digit < digits; ++digit) {
+			result[(digits - digit - 1) + 2] = hexDigit(x, digit);
+		}
+
+		return result;
+	}
+
+	private static char hexDigit(long value, int digit) {
+		return hexDigit(
+			(int) (value >>> (4 * digit))
+				& 0xF
+		);
+	}
+
+	public static char hexDigit(int value) {
+		if (value < 0xA)
+			return (char) ('0' + value);
+		else
+			return (char) ('A' - 0xA + value);
+	}
+
+	public static String replaceAll(String source, String substring, String replacement) {
+		Objects.requireNonNull(source, "source");
+		Objects.requireNonNull(substring, "substring");
+
+		if (substring.isEmpty()) {
+			throw new IllegalArgumentException("substring is empty");
+		}
+
+		if (!source.contains(substring)) { // also passes if source is empty
+			return source;
+		}
+
+		if (substring.equals(replacement)) { // null-safe
+			return source;
+		}
+
+		StringBuilder sb = new StringBuilder(2 * source.length());
+
+		for (int i = 0; i < source.length() - substring.length() + 1; ++i) {
+			if (source.startsWith(substring, i)) {
+				if (replacement != null) {
+					sb.append(replacement);
+				}
+			} else {
+				sb.append(source.charAt(i));
+			}
+		}
+
+		return sb.toString();
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/UncheckedEscapeException.java b/src/main/java/ru/windcorp/jputil/chars/UncheckedEscapeException.java
index 03c7a5f..33717ee 100644
--- a/src/main/java/ru/windcorp/jputil/chars/UncheckedEscapeException.java
+++ b/src/main/java/ru/windcorp/jputil/chars/UncheckedEscapeException.java
@@ -1,37 +1,38 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars;
-
-public class UncheckedEscapeException extends RuntimeException {
-
-	private static final long serialVersionUID = 5392628641744570926L;
-
-	public UncheckedEscapeException(String message, EscapeException cause) {
-		super(message, cause);
-	}
-
-	public UncheckedEscapeException(EscapeException cause) {
-		super(cause);
-	}
-	
-	@Override
-	public synchronized EscapeException getCause() {
-		return (EscapeException) super.getCause();
-	}
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars;
+
+public class UncheckedEscapeException extends RuntimeException {
+
+	private static final long serialVersionUID = 5392628641744570926L;
+
+	public UncheckedEscapeException(String message, EscapeException cause) {
+		super(message, cause);
+	}
+
+	public UncheckedEscapeException(EscapeException cause) {
+		super(cause);
+	}
+
+	@Override
+	public synchronized EscapeException getCause() {
+		return (EscapeException) super.getCause();
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/WordReader.java b/src/main/java/ru/windcorp/jputil/chars/WordReader.java
index 8636734..22d6969 100644
--- a/src/main/java/ru/windcorp/jputil/chars/WordReader.java
+++ b/src/main/java/ru/windcorp/jputil/chars/WordReader.java
@@ -1,139 +1,143 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars;
-
-import java.io.IOException;
-import java.io.Reader;
-import java.nio.CharBuffer;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-
-public class WordReader implements Iterator {
-	
-	private final Reader reader;
-	
-	private char[] wordBuffer = new char[1024];
-	private final CharBuffer inputBuffer;
-	
-	private String next = null;
-	private boolean isExhausted = false;
-	
-	private IOException lastException = null;
-
-	public WordReader(Reader src, int bufferSize) {
-		this.reader = src;
-		this.inputBuffer = CharBuffer.allocate(bufferSize);
-	}
-	
-	public WordReader(Reader src) {
-		this(src, 2048);
-	}
-	
-	public WordReader(char[] array, int offset, int length) {
-		this.reader = null;
-		this.inputBuffer = CharBuffer.wrap(Arrays.copyOfRange(array, offset, length + offset));
-	}
-	
-	public WordReader(char[] array) {
-		this.reader = null;
-		this.inputBuffer = CharBuffer.wrap(array);
-	}
-	
-	public WordReader(String str) {
-		this(str.toCharArray());
-	}
-	
-	@Override
-	public String next() {
-		if (!hasNext()) {
-			throw new NoSuchElementException();
-		}
-		
-		String result = next;
-		next = null;
-		return result;
-	}
-	
-	@Override
-	public boolean hasNext() {
-		if (next != null) {
-			return true;
-		}
-		
-		if (isExhausted) {
-			return false;
-		}
-		
-		int length = 0;
-		char c;
-		while (true) {
-			c = nextChar();
-			
-			if (isExhausted) break;
-			
-			if (Character.isWhitespace(c)) {
-				if (length == 0) continue;
-				else break;
-			}
-			
-			if (wordBuffer.length == length) {
-				char[] newBuf = new char[wordBuffer.length * 2];
-				System.arraycopy(wordBuffer, 0, newBuf, 0, wordBuffer.length);
-				wordBuffer = newBuf;
-			}
-			
-			wordBuffer[length++] = c;
-		}
-		
-		if (length == 0) {
-			return false;
-		}
-		
-		next = new String(wordBuffer, 0, length);
-		return true;
-	}
-
-	private char nextChar() {
-		if (!inputBuffer.hasRemaining()) {
-			if (reader == null) {
-				isExhausted = true;
-				return 0;
-			}
-			
-			inputBuffer.rewind();
-			try {
-				if (reader.read(inputBuffer) == -1) {
-					isExhausted = true;
-				}
-			} catch (IOException e) {
-				lastException = e;
-				isExhausted = true;
-				return 0;
-			}
-			
-		}
-
-		return inputBuffer.get();
-	}
-	
-	public IOException getLastException() {
-		return lastException;
-	}
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.CharBuffer;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+public class WordReader implements Iterator {
+
+	private final Reader reader;
+
+	private char[] wordBuffer = new char[1024];
+	private final CharBuffer inputBuffer;
+
+	private String next = null;
+	private boolean isExhausted = false;
+
+	private IOException lastException = null;
+
+	public WordReader(Reader src, int bufferSize) {
+		this.reader = src;
+		this.inputBuffer = CharBuffer.allocate(bufferSize);
+	}
+
+	public WordReader(Reader src) {
+		this(src, 2048);
+	}
+
+	public WordReader(char[] array, int offset, int length) {
+		this.reader = null;
+		this.inputBuffer = CharBuffer.wrap(Arrays.copyOfRange(array, offset, length + offset));
+	}
+
+	public WordReader(char[] array) {
+		this.reader = null;
+		this.inputBuffer = CharBuffer.wrap(array);
+	}
+
+	public WordReader(String str) {
+		this(str.toCharArray());
+	}
+
+	@Override
+	public String next() {
+		if (!hasNext()) {
+			throw new NoSuchElementException();
+		}
+
+		String result = next;
+		next = null;
+		return result;
+	}
+
+	@Override
+	public boolean hasNext() {
+		if (next != null) {
+			return true;
+		}
+
+		if (isExhausted) {
+			return false;
+		}
+
+		int length = 0;
+		char c;
+		while (true) {
+			c = nextChar();
+
+			if (isExhausted)
+				break;
+
+			if (Character.isWhitespace(c)) {
+				if (length == 0)
+					continue;
+				else
+					break;
+			}
+
+			if (wordBuffer.length == length) {
+				char[] newBuf = new char[wordBuffer.length * 2];
+				System.arraycopy(wordBuffer, 0, newBuf, 0, wordBuffer.length);
+				wordBuffer = newBuf;
+			}
+
+			wordBuffer[length++] = c;
+		}
+
+		if (length == 0) {
+			return false;
+		}
+
+		next = new String(wordBuffer, 0, length);
+		return true;
+	}
+
+	private char nextChar() {
+		if (!inputBuffer.hasRemaining()) {
+			if (reader == null) {
+				isExhausted = true;
+				return 0;
+			}
+
+			inputBuffer.rewind();
+			try {
+				if (reader.read(inputBuffer) == -1) {
+					isExhausted = true;
+				}
+			} catch (IOException e) {
+				lastException = e;
+				isExhausted = true;
+				return 0;
+			}
+
+		}
+
+		return inputBuffer.get();
+	}
+
+	public IOException getLastException() {
+		return lastException;
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/reader/AbstractCharReader.java b/src/main/java/ru/windcorp/jputil/chars/reader/AbstractCharReader.java
index 1d48158..d31e092 100644
--- a/src/main/java/ru/windcorp/jputil/chars/reader/AbstractCharReader.java
+++ b/src/main/java/ru/windcorp/jputil/chars/reader/AbstractCharReader.java
@@ -1,104 +1,108 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars.reader;
-
-/**
- * @author Javapony
- *
- */
-public abstract class AbstractCharReader implements CharReader {
-
-	protected static final int DEFAULT_MARK_STACK_SIZE = 8;
-	
-	/**
-	 * Current position of this CharReader. The reader maps its input to positions starting from 0.
-	 * Positions that are negative or lower than 0 are invalid. {@link #current()}
-	 * will throw an exception if position is invalid.
-	 */
-	protected int position = 0;
-	
-	private int[] marks = new int[DEFAULT_MARK_STACK_SIZE];
-	private int nextMark = 0;
-
-	protected static int closestGreaterPowerOf2(int x) {
-	    x |= x >> 1;
-	    x |= x >> 2;
-	    x |= x >> 4;
-	    x |= x >> 8;
-	    x |= x >> 16;
-	    return x + 1;
-	}
-
-	/**
-	 * @see ru.windcorp.jputil.chars.reader.CharReader#getPosition()
-	 */
-	@Override
-	public int getPosition() {
-		return position;
-	}
-
-	/**
-	 * @see ru.windcorp.jputil.chars.reader.CharReader#setPosition(int)
-	 */
-	@Override
-	public int setPosition(int position) {
-		this.position = position;
-		return position;
-	}
-
-	/**
-	 * @see ru.windcorp.jputil.chars.reader.CharReader#mark()
-	 */
-	@Override
-	public int mark() {
-		ensureMarksCapacity();
-		marks[nextMark++] = position;
-		return position;
-	}
-
-	/**
-	 * @see ru.windcorp.jputil.chars.reader.CharReader#forget()
-	 */
-	@Override
-	public int forget() {
-		return marks[--nextMark];
-	}
-
-	private void ensureMarksCapacity() {
-		if (nextMark < marks.length) return;
-		int[] newMarks = new int[closestGreaterPowerOf2(nextMark)];
-		System.arraycopy(marks, 0, newMarks, 0, nextMark);
-		marks = newMarks;
-	}
-	
-	@Override
-	public String toString() {
-		StringBuilder sb = new StringBuilder("\"");
-		
-		mark();
-		position = 0;
-		sb.append(getChars());
-		reset();
-		
-		sb.append("\"\n ");
-		for (int i = 0; i < position; ++i) sb.append(' ');
-		sb.append("^ (pos " + position + ")");
-		return sb.toString();
-	}
-
-}
\ No newline at end of file
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars.reader;
+
+/**
+ * @author Javapony
+ */
+public abstract class AbstractCharReader implements CharReader {
+
+	protected static final int DEFAULT_MARK_STACK_SIZE = 8;
+
+	/**
+	 * Current position of this CharReader. The reader maps its input to
+	 * positions starting from 0.
+	 * Positions that are negative or lower than 0 are invalid.
+	 * {@link #current()}
+	 * will throw an exception if position is invalid.
+	 */
+	protected int position = 0;
+
+	private int[] marks = new int[DEFAULT_MARK_STACK_SIZE];
+	private int nextMark = 0;
+
+	protected static int closestGreaterPowerOf2(int x) {
+		x |= x >> 1;
+		x |= x >> 2;
+		x |= x >> 4;
+		x |= x >> 8;
+		x |= x >> 16;
+		return x + 1;
+	}
+
+	/**
+	 * @see ru.windcorp.jputil.chars.reader.CharReader#getPosition()
+	 */
+	@Override
+	public int getPosition() {
+		return position;
+	}
+
+	/**
+	 * @see ru.windcorp.jputil.chars.reader.CharReader#setPosition(int)
+	 */
+	@Override
+	public int setPosition(int position) {
+		this.position = position;
+		return position;
+	}
+
+	/**
+	 * @see ru.windcorp.jputil.chars.reader.CharReader#mark()
+	 */
+	@Override
+	public int mark() {
+		ensureMarksCapacity();
+		marks[nextMark++] = position;
+		return position;
+	}
+
+	/**
+	 * @see ru.windcorp.jputil.chars.reader.CharReader#forget()
+	 */
+	@Override
+	public int forget() {
+		return marks[--nextMark];
+	}
+
+	private void ensureMarksCapacity() {
+		if (nextMark < marks.length)
+			return;
+		int[] newMarks = new int[closestGreaterPowerOf2(nextMark)];
+		System.arraycopy(marks, 0, newMarks, 0, nextMark);
+		marks = newMarks;
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder sb = new StringBuilder("\"");
+
+		mark();
+		position = 0;
+		sb.append(getChars());
+		reset();
+
+		sb.append("\"\n ");
+		for (int i = 0; i < position; ++i)
+			sb.append(' ');
+		sb.append("^ (pos " + position + ")");
+		return sb.toString();
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/reader/ArrayCharReader.java b/src/main/java/ru/windcorp/jputil/chars/reader/ArrayCharReader.java
index 13a32d2..f12f590 100644
--- a/src/main/java/ru/windcorp/jputil/chars/reader/ArrayCharReader.java
+++ b/src/main/java/ru/windcorp/jputil/chars/reader/ArrayCharReader.java
@@ -1,59 +1,60 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars.reader;
-
-import java.util.Objects;
-
-import ru.windcorp.jputil.ArrayUtil;
-
-/**
- * @author Javapony
- *
- */
-public class ArrayCharReader extends AbstractCharReader {
-	
-	private final char[] array;
-	private final int offset;
-	private final int length;
-
-	public ArrayCharReader(char[] array, int offset, int length) {
-		this.array = Objects.requireNonNull(array, "array");
-		this.length = ArrayUtil.checkArrayOffsetLength(array, offset, length);
-		this.offset = offset;
-	}
-
-	/**
-	 * @see ru.windcorp.jputil.chars.reader.CharReader#current()
-	 */
-	@Override
-	public char current() {
-		if (position >= length) return DONE;
-		if (position < 0) 
-			throw new IllegalStateException("Position " + position + " is invalid");
-		return array[position + offset];
-	}
-	
-	/**
-	 * @see ru.windcorp.jputil.chars.reader.CharReader#remaining()
-	 */
-	@Override
-	public int remaining() {
-		return length - position;
-	}
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars.reader;
+
+import java.util.Objects;
+
+import ru.windcorp.jputil.ArrayUtil;
+
+/**
+ * @author Javapony
+ */
+public class ArrayCharReader extends AbstractCharReader {
+
+	private final char[] array;
+	private final int offset;
+	private final int length;
+
+	public ArrayCharReader(char[] array, int offset, int length) {
+		this.array = Objects.requireNonNull(array, "array");
+		this.length = ArrayUtil.checkArrayOffsetLength(array, offset, length);
+		this.offset = offset;
+	}
+
+	/**
+	 * @see ru.windcorp.jputil.chars.reader.CharReader#current()
+	 */
+	@Override
+	public char current() {
+		if (position >= length)
+			return DONE;
+		if (position < 0)
+			throw new IllegalStateException("Position " + position + " is invalid");
+		return array[position + offset];
+	}
+
+	/**
+	 * @see ru.windcorp.jputil.chars.reader.CharReader#remaining()
+	 */
+	@Override
+	public int remaining() {
+		return length - position;
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/reader/BufferedCharReader.java b/src/main/java/ru/windcorp/jputil/chars/reader/BufferedCharReader.java
index 5c6d5bd..83ae6b2 100644
--- a/src/main/java/ru/windcorp/jputil/chars/reader/BufferedCharReader.java
+++ b/src/main/java/ru/windcorp/jputil/chars/reader/BufferedCharReader.java
@@ -1,122 +1,128 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars.reader;
-
-/**
- * @author Javapony
- *
- */
-public abstract class BufferedCharReader extends AbstractCharReader {
-	
-	protected static final int DEFAULT_BUFFER_SIZE = 256;
-	/**
-	 * Buffer to store data acquired with {@link #pullChars(char[], int, int)}.
-	 * Contains characters for positions [0; bufferNextIndex).
-	 */
-	private char[] buffer = new char[DEFAULT_BUFFER_SIZE];
-	
-	/**
-	 * The index of the next character.
-	 */
-	private int bufferNextIndex = 0;
-	
-	/**
-	 * Whether this reader has been buffered completely.
-	 */
-	private boolean exhausted = false;
-
-	/**
-	 * Acquires the next character.
-	 * @return the character or {@link #DONE} if the end of the reader has been reached
-	 */
-	protected abstract char pullChar();
-
-	/**
-	 * Acquires next characters and stores them in the array.
-	 * 
-	 * @param buffer the output array
-	 * @param offset index of the first character
-	 * @param length maximum amount of characters to be pulled
-	 * @return the amount of characters actually pulled
-	 */
-	protected int pullChars(char[] buffer, int offset, int length) {
-		for (int i = 0; i < length; ++i) {
-			if ((buffer[offset + i] = pullChar()) == DONE) {
-				return i;
-			}
-		}
-		
-		return length;
-	}
-	
-	private int pullChars(int offset, int length) {
-		if (exhausted || length == 0) return 0;
-		
-		int pulled = pullChars(buffer, offset, length);
-		if (pulled != length) {
-			exhausted = true;
-		}
-		
-		return pulled;
-	}
-	
-	@Override
-	public char current() {
-		if (getPosition() < 0) {
-			throw new IllegalStateException("Position " + getPosition() + " is invalid");
-		}
-			
-		if (getPosition() >= bufferNextIndex) {
-			if (exhausted) return DONE;
-			
-			ensureBufferCapacity();
-			
-			int needToPull = getPosition() - bufferNextIndex + 1;
-			assert needToPull <= buffer.length : "buffer size not ensured!";
-			
-			int pulled = pullChars(bufferNextIndex, needToPull);
-			bufferNextIndex += pulled;
-			
-			if (exhausted) return DONE;
-		}
-		
-		// TODO test the shit out of current()
-		
-		return buffer[getPosition()];
-	}
-
-	private void ensureBufferCapacity() {
-		if (getPosition() < buffer.length) return;
-		char[] newBuffer = new char[closestGreaterPowerOf2(getPosition())];
-		System.arraycopy(buffer, 0, newBuffer, 0, bufferNextIndex);
-		buffer = newBuffer;
-	}
-	
-	/**
-	 * @see ru.windcorp.jputil.chars.reader.CharReader#remaining()
-	 */
-	@Override
-	public int remaining() {
-		if (exhausted) {
-			return Math.max(bufferNextIndex - getPosition(), 0);
-		}
-		
-		return super.remaining();
-	}
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars.reader;
+
+/**
+ * @author Javapony
+ */
+public abstract class BufferedCharReader extends AbstractCharReader {
+
+	protected static final int DEFAULT_BUFFER_SIZE = 256;
+	/**
+	 * Buffer to store data acquired with {@link #pullChars(char[], int, int)}.
+	 * Contains characters for positions [0; bufferNextIndex).
+	 */
+	private char[] buffer = new char[DEFAULT_BUFFER_SIZE];
+
+	/**
+	 * The index of the next character.
+	 */
+	private int bufferNextIndex = 0;
+
+	/**
+	 * Whether this reader has been buffered completely.
+	 */
+	private boolean exhausted = false;
+
+	/**
+	 * Acquires the next character.
+	 * 
+	 * @return the character or {@link #DONE} if the end of the reader has been
+	 *         reached
+	 */
+	protected abstract char pullChar();
+
+	/**
+	 * Acquires next characters and stores them in the array.
+	 * 
+	 * @param buffer the output array
+	 * @param offset index of the first character
+	 * @param length maximum amount of characters to be pulled
+	 * @return the amount of characters actually pulled
+	 */
+	protected int pullChars(char[] buffer, int offset, int length) {
+		for (int i = 0; i < length; ++i) {
+			if ((buffer[offset + i] = pullChar()) == DONE) {
+				return i;
+			}
+		}
+
+		return length;
+	}
+
+	private int pullChars(int offset, int length) {
+		if (exhausted || length == 0)
+			return 0;
+
+		int pulled = pullChars(buffer, offset, length);
+		if (pulled != length) {
+			exhausted = true;
+		}
+
+		return pulled;
+	}
+
+	@Override
+	public char current() {
+		if (getPosition() < 0) {
+			throw new IllegalStateException("Position " + getPosition() + " is invalid");
+		}
+
+		if (getPosition() >= bufferNextIndex) {
+			if (exhausted)
+				return DONE;
+
+			ensureBufferCapacity();
+
+			int needToPull = getPosition() - bufferNextIndex + 1;
+			assert needToPull <= buffer.length : "buffer size not ensured!";
+
+			int pulled = pullChars(bufferNextIndex, needToPull);
+			bufferNextIndex += pulled;
+
+			if (exhausted)
+				return DONE;
+		}
+
+		// TODO test the shit out of current()
+
+		return buffer[getPosition()];
+	}
+
+	private void ensureBufferCapacity() {
+		if (getPosition() < buffer.length)
+			return;
+		char[] newBuffer = new char[closestGreaterPowerOf2(getPosition())];
+		System.arraycopy(buffer, 0, newBuffer, 0, bufferNextIndex);
+		buffer = newBuffer;
+	}
+
+	/**
+	 * @see ru.windcorp.jputil.chars.reader.CharReader#remaining()
+	 */
+	@Override
+	public int remaining() {
+		if (exhausted) {
+			return Math.max(bufferNextIndex - getPosition(), 0);
+		}
+
+		return super.remaining();
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/reader/CharReader.java b/src/main/java/ru/windcorp/jputil/chars/reader/CharReader.java
index c6dd90a..147daf3 100644
--- a/src/main/java/ru/windcorp/jputil/chars/reader/CharReader.java
+++ b/src/main/java/ru/windcorp/jputil/chars/reader/CharReader.java
@@ -1,270 +1,284 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars.reader;
-
-import java.io.IOException;
-import java.util.Arrays;
-
-import ru.windcorp.jputil.chars.CharPredicate;
-import ru.windcorp.jputil.chars.EscapeException;
-import ru.windcorp.jputil.chars.Escaper;
-
-/**
- * @author Javapony
- *
- */
-
-// SonarLint: Constants should not be defined in interfaces (java:S1214)
-//   DONE is an essential part of the interface
-@SuppressWarnings("squid:S1214")
-
-public interface CharReader {
-
-	char DONE = '\uFFFF';
-
-	char current();
-	int getPosition();
-	int setPosition(int position);
-	
-	default char next() {
-		return advance(1);
-	}
-	
-	default char previous() {
-		return rollBack(1);
-	}
-	
-	default char consume() {
-		char c = current();
-		advance(1);
-		return c;
-	}
-	
-	default char advance(int forward) {
-		setPosition(getPosition() + forward);
-		return current();
-	}
-	
-	default char rollBack(int backward) {
-		return advance(-backward);
-	}
-	
-	default boolean isEnd() {
-		return current() == DONE;
-	}
-	
-	default boolean has() {
-		return current() != DONE;
-	}
-	
-	default boolean is(char c) {
-		return current() == c;
-	}
-	
-	default int getChars(char[] output, int offset, int length) {
-		for (int i = 0; i < length; ++i) {
-			if ((output[offset + i] = current()) == DONE) {
-				return i;
-			}
-			next();
-		}
-		
-		return length;
-	}
-	
-	default int getChars(char[] output) {
-		return getChars(output, 0, output.length);
-	}
-	
-	default char[] getChars(int length) {
-		char[] result = new char[length];
-		int from = getChars(result);
-		if (from != length) Arrays.fill(result, from, length, DONE);
-		return result;
-	}
-	
-	default char[] getChars() {
-		return getChars(remaining());
-	}
-	
-	default String getString(int length) {
-		StringBuilder sb = new StringBuilder();
-		for (int i = 0; i < length && !isEnd(); ++i) sb.append(consume());
-		return sb.toString();
-	}
-	
-	default String getString() {
-		return getString(Integer.MAX_VALUE);
-	}
-	
-	default boolean match(CharSequence seq) {
-		for (int i = 0; i < seq.length(); ++i) {
-			if (isEnd()) return false;
-			if (current() != seq.charAt(i)) return false;
-			next();
-		}
-		
-		return true;
-	}
-	
-	default boolean matchOrReset(CharSequence seq) {
-		mark();
-		if (match(seq)) {
-			forget();
-			return true;
-		} else {
-			reset();
-			return false;
-		}
-	}
-	
-	default boolean match(char[] array) {
-		for (int i = 0; i < array.length; ++i) {
-			if (isEnd()) return false;
-			if (current() != array[i]) return false;
-			next();
-		}
-		
-		return true;
-	}
-	
-	default boolean matchOrReset(char[] array) {
-		mark();
-		if (match(array)) {
-			forget();
-			return true;
-		} else {
-			reset();
-			return false;
-		}
-	}
-	
-	default int skip(CharPredicate condition) {
-		int i = 0;
-		
-		while (has() && condition.test(current())) {
-			i++;
-			next();
-		}
-		
-		return i;
-	}
-	
-	default int skipWhitespace() {
-		return skip(Character::isWhitespace);
-	}
-	
-	/**
-	 * Skips to the end of the current line. Both "\n", "\r"
-	 * and "\r\n" are considered line separators.
-	 * @return the amount of characters in the skipped line
-	 */
-	default int skipLine() {
-		int i = 0;
-		
-		while (!isEnd()) {
-			if (current() == '\r') {
-				if (next() == '\n') {
-					next();
-				}
-				break;
-			} else if (current() == '\n') {
-				next();
-				break;
-			}
-			
-			i++;
-			next();
-		}
-		
-		return i;
-	}
-	
-	default char[] readWhile(CharPredicate condition) {
-		return readUntil(CharPredicate.negate(condition));
-	}
-	
-	default char[] readUntil(CharPredicate condition) {
-		mark();
-		int length = 0;
-		while (!isEnd() && !condition.test(current())) {
-			length++;
-			next();
-		}
-		reset();
-		
-		char[] result = new char[length];
-		for (int i = 0; i < length; ++i) result[i] = consume();
-		return result;
-	}
-	
-	default char[] readWord() {
-		skipWhitespace();
-		return readUntil(Character::isWhitespace);
-	}
-	
-	default char[] readWord(Escaper escaper, char quotes) throws EscapeException {
-		skipWhitespace();
-		
-		if (current() == quotes) {
-			return escaper.unescape(this, quotes);
-		} else {
-			return readWord();
-		}
-	}
-	
-	default char[] readLine() {
-		mark();
-		int length = skipLine();
-		reset();
-		
-		char[] result = new char[length];
-		for (int i = 0; i < result.length; ++i) result[i] = consume();
-		return result;
-	}
-	
-	default int remaining() {
-		mark();
-		int result = 0;
-		
-		while (consume() != DONE) result++;
-		
-		reset();
-		return result;
-	}
-
-	int mark();
-	int forget();
-	
-	default int reset() {
-		return setPosition(forget());
-	}
-	
-	default IOException getLastException() {
-		return null;
-	}
-	
-	default void resetLastException() {
-		// Do nothing
-	}
-	
-	default boolean hasErrored() {
-		return getLastException() != null;
-	}
-
-}
\ No newline at end of file
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars.reader;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import ru.windcorp.jputil.chars.CharPredicate;
+import ru.windcorp.jputil.chars.EscapeException;
+import ru.windcorp.jputil.chars.Escaper;
+
+/**
+ * @author Javapony
+ */
+
+// SonarLint: Constants should not be defined in interfaces (java:S1214)
+//   DONE is an essential part of the interface
+@SuppressWarnings("squid:S1214")
+
+public interface CharReader {
+
+	char DONE = '\uFFFF';
+
+	char current();
+
+	int getPosition();
+
+	int setPosition(int position);
+
+	default char next() {
+		return advance(1);
+	}
+
+	default char previous() {
+		return rollBack(1);
+	}
+
+	default char consume() {
+		char c = current();
+		advance(1);
+		return c;
+	}
+
+	default char advance(int forward) {
+		setPosition(getPosition() + forward);
+		return current();
+	}
+
+	default char rollBack(int backward) {
+		return advance(-backward);
+	}
+
+	default boolean isEnd() {
+		return current() == DONE;
+	}
+
+	default boolean has() {
+		return current() != DONE;
+	}
+
+	default boolean is(char c) {
+		return current() == c;
+	}
+
+	default int getChars(char[] output, int offset, int length) {
+		for (int i = 0; i < length; ++i) {
+			if ((output[offset + i] = current()) == DONE) {
+				return i;
+			}
+			next();
+		}
+
+		return length;
+	}
+
+	default int getChars(char[] output) {
+		return getChars(output, 0, output.length);
+	}
+
+	default char[] getChars(int length) {
+		char[] result = new char[length];
+		int from = getChars(result);
+		if (from != length)
+			Arrays.fill(result, from, length, DONE);
+		return result;
+	}
+
+	default char[] getChars() {
+		return getChars(remaining());
+	}
+
+	default String getString(int length) {
+		StringBuilder sb = new StringBuilder();
+		for (int i = 0; i < length && !isEnd(); ++i)
+			sb.append(consume());
+		return sb.toString();
+	}
+
+	default String getString() {
+		return getString(Integer.MAX_VALUE);
+	}
+
+	default boolean match(CharSequence seq) {
+		for (int i = 0; i < seq.length(); ++i) {
+			if (isEnd())
+				return false;
+			if (current() != seq.charAt(i))
+				return false;
+			next();
+		}
+
+		return true;
+	}
+
+	default boolean matchOrReset(CharSequence seq) {
+		mark();
+		if (match(seq)) {
+			forget();
+			return true;
+		} else {
+			reset();
+			return false;
+		}
+	}
+
+	default boolean match(char[] array) {
+		for (int i = 0; i < array.length; ++i) {
+			if (isEnd())
+				return false;
+			if (current() != array[i])
+				return false;
+			next();
+		}
+
+		return true;
+	}
+
+	default boolean matchOrReset(char[] array) {
+		mark();
+		if (match(array)) {
+			forget();
+			return true;
+		} else {
+			reset();
+			return false;
+		}
+	}
+
+	default int skip(CharPredicate condition) {
+		int i = 0;
+
+		while (has() && condition.test(current())) {
+			i++;
+			next();
+		}
+
+		return i;
+	}
+
+	default int skipWhitespace() {
+		return skip(Character::isWhitespace);
+	}
+
+	/**
+	 * Skips to the end of the current line. Both "\n",
+	 * "\r"
+	 * and "\r\n" are considered line separators.
+	 * 
+	 * @return the amount of characters in the skipped line
+	 */
+	default int skipLine() {
+		int i = 0;
+
+		while (!isEnd()) {
+			if (current() == '\r') {
+				if (next() == '\n') {
+					next();
+				}
+				break;
+			} else if (current() == '\n') {
+				next();
+				break;
+			}
+
+			i++;
+			next();
+		}
+
+		return i;
+	}
+
+	default char[] readWhile(CharPredicate condition) {
+		return readUntil(CharPredicate.negate(condition));
+	}
+
+	default char[] readUntil(CharPredicate condition) {
+		mark();
+		int length = 0;
+		while (!isEnd() && !condition.test(current())) {
+			length++;
+			next();
+		}
+		reset();
+
+		char[] result = new char[length];
+		for (int i = 0; i < length; ++i)
+			result[i] = consume();
+		return result;
+	}
+
+	default char[] readWord() {
+		skipWhitespace();
+		return readUntil(Character::isWhitespace);
+	}
+
+	default char[] readWord(Escaper escaper, char quotes) throws EscapeException {
+		skipWhitespace();
+
+		if (current() == quotes) {
+			return escaper.unescape(this, quotes);
+		} else {
+			return readWord();
+		}
+	}
+
+	default char[] readLine() {
+		mark();
+		int length = skipLine();
+		reset();
+
+		char[] result = new char[length];
+		for (int i = 0; i < result.length; ++i)
+			result[i] = consume();
+		return result;
+	}
+
+	default int remaining() {
+		mark();
+		int result = 0;
+
+		while (consume() != DONE)
+			result++;
+
+		reset();
+		return result;
+	}
+
+	int mark();
+
+	int forget();
+
+	default int reset() {
+		return setPosition(forget());
+	}
+
+	default IOException getLastException() {
+		return null;
+	}
+
+	default void resetLastException() {
+		// Do nothing
+	}
+
+	default boolean hasErrored() {
+		return getLastException() != null;
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/reader/CharReaders.java b/src/main/java/ru/windcorp/jputil/chars/reader/CharReaders.java
index 9cd02e0..a181dbf 100644
--- a/src/main/java/ru/windcorp/jputil/chars/reader/CharReaders.java
+++ b/src/main/java/ru/windcorp/jputil/chars/reader/CharReaders.java
@@ -1,112 +1,113 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars.reader;
-
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.text.CharacterIterator;
-import java.util.function.IntSupplier;
-
-import ru.windcorp.jputil.chars.CharSupplier;
-
-/**
- * @author Javapony
- *
- */
-public class CharReaders {
-	
-	private CharReaders() {}
-	
-	public static CharReader wrap(char[] array, int offset, int length) {
-		return new ArrayCharReader(array, offset, length);
-	}
-	
-	public static CharReader wrap(char[] array) {
-		return wrap(array, 0, array.length);
-	}
-	
-	public static CharReader wrap(String str, int offset, int length) {
-		return new StringCharReader(str, offset, length);
-	}
-	
-	public static CharReader wrap(String str) {
-		return wrap(str, 0, str.length());
-	}
-	
-	public static CharReader wrap(CharSupplier supplier) {
-		return new BufferedCharReader() {
-			@Override
-			protected char pullChar() {
-				try {
-					return supplier.getAsChar();
-				} catch (Exception e) {
-					return DONE;
-				}
-			}
-		};
-	}
-	
-	public static CharReader wrap(IntSupplier supplier) {
-		return new BufferedCharReader() {
-			@Override
-			protected char pullChar() {
-				try {
-					int i = supplier.getAsInt();
-					if (i < 0 || i > Character.MAX_VALUE) {
-						return DONE;
-					} else {
-						return (char) i;
-					}
-				} catch (Exception e) {
-					return DONE;
-				}
-			}
-		};
-	}
-	
-	public static CharReader wrap(CharacterIterator it) {
-		return new BufferedCharReader() {
-			@Override
-			protected char pullChar() {
-				char result = it.current();
-				it.next();
-				return result;
-			}
-		};
-	}
-	
-	public static CharReader wrap(Reader reader) {
-		return new ReaderCharReader(reader);
-	}
-	
-	public static CharReader wrap(InputStream is, Charset charset) {
-		return wrap(new InputStreamReader(is, charset));
-	}
-	
-	public static CharReader wrapDefaultCS(InputStream is) {
-		return wrap(new InputStreamReader(is));
-	}
-	
-	public static CharReader wrapUTF8(InputStream is) {
-		return wrap(new InputStreamReader(is, StandardCharsets.UTF_8));
-	}
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars.reader;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.text.CharacterIterator;
+import java.util.function.IntSupplier;
+
+import ru.windcorp.jputil.chars.CharSupplier;
+
+/**
+ * @author Javapony
+ */
+public class CharReaders {
+
+	private CharReaders() {
+	}
+
+	public static CharReader wrap(char[] array, int offset, int length) {
+		return new ArrayCharReader(array, offset, length);
+	}
+
+	public static CharReader wrap(char[] array) {
+		return wrap(array, 0, array.length);
+	}
+
+	public static CharReader wrap(String str, int offset, int length) {
+		return new StringCharReader(str, offset, length);
+	}
+
+	public static CharReader wrap(String str) {
+		return wrap(str, 0, str.length());
+	}
+
+	public static CharReader wrap(CharSupplier supplier) {
+		return new BufferedCharReader() {
+			@Override
+			protected char pullChar() {
+				try {
+					return supplier.getAsChar();
+				} catch (Exception e) {
+					return DONE;
+				}
+			}
+		};
+	}
+
+	public static CharReader wrap(IntSupplier supplier) {
+		return new BufferedCharReader() {
+			@Override
+			protected char pullChar() {
+				try {
+					int i = supplier.getAsInt();
+					if (i < 0 || i > Character.MAX_VALUE) {
+						return DONE;
+					} else {
+						return (char) i;
+					}
+				} catch (Exception e) {
+					return DONE;
+				}
+			}
+		};
+	}
+
+	public static CharReader wrap(CharacterIterator it) {
+		return new BufferedCharReader() {
+			@Override
+			protected char pullChar() {
+				char result = it.current();
+				it.next();
+				return result;
+			}
+		};
+	}
+
+	public static CharReader wrap(Reader reader) {
+		return new ReaderCharReader(reader);
+	}
+
+	public static CharReader wrap(InputStream is, Charset charset) {
+		return wrap(new InputStreamReader(is, charset));
+	}
+
+	public static CharReader wrapDefaultCS(InputStream is) {
+		return wrap(new InputStreamReader(is));
+	}
+
+	public static CharReader wrapUTF8(InputStream is) {
+		return wrap(new InputStreamReader(is, StandardCharsets.UTF_8));
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/reader/ReaderCharReader.java b/src/main/java/ru/windcorp/jputil/chars/reader/ReaderCharReader.java
index 0a2435f..51a88db 100644
--- a/src/main/java/ru/windcorp/jputil/chars/reader/ReaderCharReader.java
+++ b/src/main/java/ru/windcorp/jputil/chars/reader/ReaderCharReader.java
@@ -1,70 +1,71 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars.reader;
-
-import java.io.IOException;
-import java.io.Reader;
-
-/**
- * @author Javapony
- *
- */
-public class ReaderCharReader extends BufferedCharReader {
-	
-	private final Reader src;
-	private IOException lastException = null;
-
-	public ReaderCharReader(Reader src) {
-		this.src = src;
-	}
-
-	/**
-	 * @see ru.windcorp.jputil.chars.reader.BufferedCharReader#pullChar()
-	 */
-	@Override
-	protected char pullChar() {
-		try {
-			return (char) src.read(); // Handles DONE correctly
-		} catch (IOException e) {
-			lastException = e;
-			return DONE;
-		}
-	}
-	
-	/**
-	 * @see ru.windcorp.jputil.chars.reader.BufferedCharReader#pullChars(char[], int, int)
-	 */
-	@Override
-	protected int pullChars(char[] buffer, int offset, int length) {
-		try {
-			return src.read(buffer, offset, length);
-		} catch (IOException e) {
-			lastException = e;
-			return 0;
-		}
-	}
-
-	/**
-	 * @return the exception
-	 */
-	@Override
-	public IOException getLastException() {
-		return lastException;
-	}
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars.reader;
+
+import java.io.IOException;
+import java.io.Reader;
+
+/**
+ * @author Javapony
+ */
+public class ReaderCharReader extends BufferedCharReader {
+
+	private final Reader src;
+	private IOException lastException = null;
+
+	public ReaderCharReader(Reader src) {
+		this.src = src;
+	}
+
+	/**
+	 * @see ru.windcorp.jputil.chars.reader.BufferedCharReader#pullChar()
+	 */
+	@Override
+	protected char pullChar() {
+		try {
+			return (char) src.read(); // Handles DONE correctly
+		} catch (IOException e) {
+			lastException = e;
+			return DONE;
+		}
+	}
+
+	/**
+	 * @see ru.windcorp.jputil.chars.reader.BufferedCharReader#pullChars(char[],
+	 *      int, int)
+	 */
+	@Override
+	protected int pullChars(char[] buffer, int offset, int length) {
+		try {
+			return src.read(buffer, offset, length);
+		} catch (IOException e) {
+			lastException = e;
+			return 0;
+		}
+	}
+
+	/**
+	 * @return the exception
+	 */
+	@Override
+	public IOException getLastException() {
+		return lastException;
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/chars/reader/StringCharReader.java b/src/main/java/ru/windcorp/jputil/chars/reader/StringCharReader.java
index c082321..5836d15 100644
--- a/src/main/java/ru/windcorp/jputil/chars/reader/StringCharReader.java
+++ b/src/main/java/ru/windcorp/jputil/chars/reader/StringCharReader.java
@@ -1,65 +1,68 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.chars.reader;
-
-import java.util.Objects;
-
-/**
- * @author Javapony
- *
- */
-public class StringCharReader extends AbstractCharReader {
-	
-	private final String str;
-	private final int offset;
-	private final int length;
-
-	public StringCharReader(String str, int offset, int length) {
-		this.str = Objects.requireNonNull(str, "str");
-		
-		if (length < 0)
-			length = str.length();
-		
-		int end = offset + length;
-		if (end > str.length() || offset < 0)
-			throw new IllegalArgumentException("String contains [0; " + str.length() + "), requested [" + offset + "; " + end + ")");
-		
-		this.offset = offset;
-		this.length = length;
-	}
-
-	/**
-	 * @see ru.windcorp.jputil.chars.reader.CharReader#current()
-	 */
-	@Override
-	public char current() {
-		if (position >= length) return DONE;
-		if (position < 0) 
-			throw new IllegalStateException("Position " + position + " is invalid");
-		return str.charAt(position + offset);
-	}
-	
-	/**
-	 * @see ru.windcorp.jputil.chars.reader.CharReader#remaining()
-	 */
-	@Override
-	public int remaining() {
-		return length - position;
-	}
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.chars.reader;
+
+import java.util.Objects;
+
+/**
+ * @author Javapony
+ */
+public class StringCharReader extends AbstractCharReader {
+
+	private final String str;
+	private final int offset;
+	private final int length;
+
+	public StringCharReader(String str, int offset, int length) {
+		this.str = Objects.requireNonNull(str, "str");
+
+		if (length < 0)
+			length = str.length();
+
+		int end = offset + length;
+		if (end > str.length() || offset < 0)
+			throw new IllegalArgumentException(
+				"String contains [0; " + str.length() + "), requested [" + offset + "; " + end + ")"
+			);
+
+		this.offset = offset;
+		this.length = length;
+	}
+
+	/**
+	 * @see ru.windcorp.jputil.chars.reader.CharReader#current()
+	 */
+	@Override
+	public char current() {
+		if (position >= length)
+			return DONE;
+		if (position < 0)
+			throw new IllegalStateException("Position " + position + " is invalid");
+		return str.charAt(position + offset);
+	}
+
+	/**
+	 * @see ru.windcorp.jputil.chars.reader.CharReader#remaining()
+	 */
+	@Override
+	public int remaining() {
+		return length - position;
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/functions/FloatSupplier.java b/src/main/java/ru/windcorp/jputil/functions/FloatSupplier.java
index 060b06a..a157032 100644
--- a/src/main/java/ru/windcorp/jputil/functions/FloatSupplier.java
+++ b/src/main/java/ru/windcorp/jputil/functions/FloatSupplier.java
@@ -1,8 +1,26 @@
-package ru.windcorp.jputil.functions;
-
-@FunctionalInterface
-public interface FloatSupplier {
-	
-	float getAsFloat();
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.functions;
+
+@FunctionalInterface
+public interface FloatSupplier {
+
+	float getAsFloat();
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/functions/ThrowingBiConsumer.java b/src/main/java/ru/windcorp/jputil/functions/ThrowingBiConsumer.java
index 4e1c26e..be7cb8f 100644
--- a/src/main/java/ru/windcorp/jputil/functions/ThrowingBiConsumer.java
+++ b/src/main/java/ru/windcorp/jputil/functions/ThrowingBiConsumer.java
@@ -1,72 +1,76 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.functions;
-
-import java.util.function.BiConsumer;
-
-@FunctionalInterface
-public interface ThrowingBiConsumer {
-	
-	@FunctionalInterface
-	public static interface BiConsumerHandler {
-		void handle(T t, U u, E e);
-	}
-
-	void accept(T t, U u) throws E;
-	
-	@SuppressWarnings("unchecked")
-	default BiConsumer withHandler(BiConsumerHandler super T, ? super U, ? super E> handler) {
-		return (t, u) -> {
-			try {
-				accept(t, u);
-			} catch (RuntimeException e) {
-				throw e;
-			} catch (Exception e) {
-				handler.handle(t, u, (E) e);
-			}
-		};
-	}
-	
-	public static  ThrowingBiConsumer concat(
-			ThrowingBiConsumer super T, ? super U, ? extends E> first,
-			ThrowingBiConsumer super T, ? super U, ? extends E> second) {
-		return (t, u) -> {
-			first.accept(t, u);
-			second.accept(t, u);
-		};
-	}
-	
-	public static  ThrowingBiConsumer concat(
-			BiConsumer super T, ? super U> first,
-			ThrowingBiConsumer super T, ? super U, E> second) {
-		return (t, u) -> {
-			first.accept(t, u);
-			second.accept(t, u);
-		};
-	}
-	
-	public static  ThrowingBiConsumer concat(
-			ThrowingBiConsumer super T, ? super U, E> first,
-			BiConsumer super T, ? super U> second) {
-		return (t, u) -> {
-			first.accept(t, u);
-			second.accept(t, u);
-		};
-	}
-	
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.functions;
+
+import java.util.function.BiConsumer;
+
+@FunctionalInterface
+public interface ThrowingBiConsumer {
+
+	@FunctionalInterface
+	public static interface BiConsumerHandler {
+		void handle(T t, U u, E e);
+	}
+
+	void accept(T t, U u) throws E;
+
+	@SuppressWarnings("unchecked")
+	default BiConsumer withHandler(BiConsumerHandler super T, ? super U, ? super E> handler) {
+		return (t, u) -> {
+			try {
+				accept(t, u);
+			} catch (RuntimeException e) {
+				throw e;
+			} catch (Exception e) {
+				handler.handle(t, u, (E) e);
+			}
+		};
+	}
+
+	public static  ThrowingBiConsumer concat(
+		ThrowingBiConsumer super T, ? super U, ? extends E> first,
+		ThrowingBiConsumer super T, ? super U, ? extends E> second
+	) {
+		return (t, u) -> {
+			first.accept(t, u);
+			second.accept(t, u);
+		};
+	}
+
+	public static  ThrowingBiConsumer concat(
+		BiConsumer super T, ? super U> first,
+		ThrowingBiConsumer super T, ? super U, E> second
+	) {
+		return (t, u) -> {
+			first.accept(t, u);
+			second.accept(t, u);
+		};
+	}
+
+	public static  ThrowingBiConsumer concat(
+		ThrowingBiConsumer super T, ? super U, E> first,
+		BiConsumer super T, ? super U> second
+	) {
+		return (t, u) -> {
+			first.accept(t, u);
+			second.accept(t, u);
+		};
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/functions/ThrowingConsumer.java b/src/main/java/ru/windcorp/jputil/functions/ThrowingConsumer.java
index dcd1931..14449ca 100644
--- a/src/main/java/ru/windcorp/jputil/functions/ThrowingConsumer.java
+++ b/src/main/java/ru/windcorp/jputil/functions/ThrowingConsumer.java
@@ -1,62 +1,72 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.functions;
-
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-
-@FunctionalInterface
-public interface ThrowingConsumer {
-
-	void accept(T t) throws E;
-	
-	@SuppressWarnings("unchecked")
-	default Consumer withHandler(BiConsumer super T, ? super E> handler) {
-		return t -> {
-			try {
-				accept(t);
-			} catch (RuntimeException e) {
-				throw e;
-			} catch (Exception e) {
-				handler.accept(t, (E) e);
-			}
-		};
-	}
-	
-	public static  ThrowingConsumer concat(ThrowingConsumer super T, ? extends E> first, ThrowingConsumer super T, ? extends E> second) {
-		return t -> {
-			first.accept(t);
-			second.accept(t);
-		};
-	}
-	
-	public static  ThrowingConsumer concat(Consumer super T> first, ThrowingConsumer super T, ? extends E> second) {
-		return t -> {
-			first.accept(t);
-			second.accept(t);
-		};
-	}
-	
-	public static  ThrowingConsumer concat(ThrowingConsumer super T, ? extends E> first, Consumer super T> second) {
-		return t -> {
-			first.accept(t);
-			second.accept(t);
-		};
-	}
-	
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.functions;
+
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+@FunctionalInterface
+public interface ThrowingConsumer {
+
+	void accept(T t) throws E;
+
+	@SuppressWarnings("unchecked")
+	default Consumer withHandler(BiConsumer super T, ? super E> handler) {
+		return t -> {
+			try {
+				accept(t);
+			} catch (RuntimeException e) {
+				throw e;
+			} catch (Exception e) {
+				handler.accept(t, (E) e);
+			}
+		};
+	}
+
+	public static  ThrowingConsumer concat(
+		ThrowingConsumer super T, ? extends E> first,
+		ThrowingConsumer super T, ? extends E> second
+	) {
+		return t -> {
+			first.accept(t);
+			second.accept(t);
+		};
+	}
+
+	public static  ThrowingConsumer concat(
+		Consumer super T> first,
+		ThrowingConsumer super T, ? extends E> second
+	) {
+		return t -> {
+			first.accept(t);
+			second.accept(t);
+		};
+	}
+
+	public static  ThrowingConsumer concat(
+		ThrowingConsumer super T, ? extends E> first,
+		Consumer super T> second
+	) {
+		return t -> {
+			first.accept(t);
+			second.accept(t);
+		};
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/functions/ThrowingFunction.java b/src/main/java/ru/windcorp/jputil/functions/ThrowingFunction.java
index faffb44..afdd078 100644
--- a/src/main/java/ru/windcorp/jputil/functions/ThrowingFunction.java
+++ b/src/main/java/ru/windcorp/jputil/functions/ThrowingFunction.java
@@ -1,73 +1,81 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.functions;
-
-import java.util.function.BiConsumer;
-import java.util.function.Function;
-import java.util.function.Supplier;
-
-@FunctionalInterface
-public interface ThrowingFunction {
-
-	R apply(T t) throws E;
-	
-	@SuppressWarnings("unchecked")
-	default Function withHandler(BiConsumer super T, ? super E> handler, Function super T, ? extends R> value) {
-		return t -> {
-			try {
-				return apply(t);
-			} catch (RuntimeException e) {
-				throw e;
-			} catch (Exception e) {
-				if (handler != null) handler.accept(t, (E) e);
-				return value == null ? null : value.apply(t);
-			}
-		};
-	}
-	
-	default Function withHandler(BiConsumer super T, ? super E> handler, Supplier extends R> value) {
-		return withHandler(handler, t -> value.get());
-	}
-	
-	default Function withHandler(BiConsumer super T, ? super E> handler, R value) {
-		return withHandler(handler, t -> value);
-	}
-	
-	default Function withHandler(BiConsumer super T, ? super E> handler) {
-		return withHandler(handler, (Function) null);
-	}
-	
-	public static  ThrowingFunction compose(
-			ThrowingFunction super T, I, ? extends E> first,
-			ThrowingFunction super I, ? extends R, ? extends E> second) {
-		return t -> second.apply(first.apply(t));
-	}
-	
-	public static  ThrowingFunction compose(
-			Function super T, I> first,
-			ThrowingFunction super I, ? extends R, E> second) {
-		return t -> second.apply(first.apply(t));
-	}
-	
-	public static  ThrowingFunction compose(
-			ThrowingFunction super T, I, E> first,
-			Function super I, ? extends R> second) {
-		return t -> second.apply(first.apply(t));
-	}
-	
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.functions;
+
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+@FunctionalInterface
+public interface ThrowingFunction {
+
+	R apply(T t) throws E;
+
+	@SuppressWarnings("unchecked")
+	default Function withHandler(
+		BiConsumer super T, ? super E> handler,
+		Function super T, ? extends R> value
+	) {
+		return t -> {
+			try {
+				return apply(t);
+			} catch (RuntimeException e) {
+				throw e;
+			} catch (Exception e) {
+				if (handler != null)
+					handler.accept(t, (E) e);
+				return value == null ? null : value.apply(t);
+			}
+		};
+	}
+
+	default Function withHandler(BiConsumer super T, ? super E> handler, Supplier extends R> value) {
+		return withHandler(handler, t -> value.get());
+	}
+
+	default Function withHandler(BiConsumer super T, ? super E> handler, R value) {
+		return withHandler(handler, t -> value);
+	}
+
+	default Function withHandler(BiConsumer super T, ? super E> handler) {
+		return withHandler(handler, (Function) null);
+	}
+
+	public static  ThrowingFunction compose(
+		ThrowingFunction super T, I, ? extends E> first,
+		ThrowingFunction super I, ? extends R, ? extends E> second
+	) {
+		return t -> second.apply(first.apply(t));
+	}
+
+	public static  ThrowingFunction compose(
+		Function super T, I> first,
+		ThrowingFunction super I, ? extends R, E> second
+	) {
+		return t -> second.apply(first.apply(t));
+	}
+
+	public static  ThrowingFunction compose(
+		ThrowingFunction super T, I, E> first,
+		Function super I, ? extends R> second
+	) {
+		return t -> second.apply(first.apply(t));
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/functions/ThrowingRunnable.java b/src/main/java/ru/windcorp/jputil/functions/ThrowingRunnable.java
index 0684122..f27429b 100644
--- a/src/main/java/ru/windcorp/jputil/functions/ThrowingRunnable.java
+++ b/src/main/java/ru/windcorp/jputil/functions/ThrowingRunnable.java
@@ -1,64 +1,65 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.functions;
-
-import java.util.function.Consumer;
-
-@FunctionalInterface
-public interface ThrowingRunnable {
-
-	void run() throws E;
-	
-	@SuppressWarnings("unchecked")
-	default Runnable withHandler(Consumer super E> handler) {
-		return () -> {
-			try {
-				run();
-			} catch (RuntimeException e) {
-				throw e;
-			} catch (Exception e) {
-				handler.accept((E) e);
-			}
-		};
-	}
-	
-	public static  ThrowingRunnable concat(
-			ThrowingRunnable extends E> first,
-			ThrowingRunnable extends E> second
-	) {
-		return () -> {
-			first.run();
-			second.run();
-		};
-	}
-	
-	public static  ThrowingRunnable concat(Runnable first, ThrowingRunnable second) {
-		return () -> {
-			first.run();
-			second.run();
-		};
-	}
-	
-	public static  ThrowingRunnable concat(ThrowingRunnable first, Runnable second) {
-		return () -> {
-			first.run();
-			second.run();
-		};
-	}
-	
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.functions;
+
+import java.util.function.Consumer;
+
+@FunctionalInterface
+public interface ThrowingRunnable {
+
+	void run() throws E;
+
+	@SuppressWarnings("unchecked")
+	default Runnable withHandler(Consumer super E> handler) {
+		return () -> {
+			try {
+				run();
+			} catch (RuntimeException e) {
+				throw e;
+			} catch (Exception e) {
+				handler.accept((E) e);
+			}
+		};
+	}
+
+	public static  ThrowingRunnable concat(
+		ThrowingRunnable extends E> first,
+		ThrowingRunnable extends E> second
+	) {
+		return () -> {
+			first.run();
+			second.run();
+		};
+	}
+
+	public static  ThrowingRunnable concat(Runnable first, ThrowingRunnable second) {
+		return () -> {
+			first.run();
+			second.run();
+		};
+	}
+
+	public static  ThrowingRunnable concat(ThrowingRunnable first, Runnable second) {
+		return () -> {
+			first.run();
+			second.run();
+		};
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/functions/ThrowingSupplier.java b/src/main/java/ru/windcorp/jputil/functions/ThrowingSupplier.java
index 32327e5..84ad690 100644
--- a/src/main/java/ru/windcorp/jputil/functions/ThrowingSupplier.java
+++ b/src/main/java/ru/windcorp/jputil/functions/ThrowingSupplier.java
@@ -1,50 +1,52 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.functions;
-
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-@FunctionalInterface
-public interface ThrowingSupplier {
-
-	T get() throws E;
-	
-	@SuppressWarnings("unchecked")
-	default Supplier withHandler(Consumer super E> handler, Supplier extends T> value) {
-		return () -> {
-			try {
-				return get();
-			} catch (RuntimeException e) {
-				throw e;
-			} catch (Exception e) {
-				if (handler != null) handler.accept((E) e);
-				return value == null ? null : value.get();
-			}
-		};
-	}
-	
-	default Supplier withHandler(Consumer super E> handler, T value) {
-		return withHandler(handler, () -> value);
-	}
-	
-	default Supplier withHandler(Consumer super E> handler) {
-		return withHandler(handler, (Supplier) null);
-	}
-	
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.functions;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+@FunctionalInterface
+public interface ThrowingSupplier {
+
+	T get() throws E;
+
+	@SuppressWarnings("unchecked")
+	default Supplier withHandler(Consumer super E> handler, Supplier extends T> value) {
+		return () -> {
+			try {
+				return get();
+			} catch (RuntimeException e) {
+				throw e;
+			} catch (Exception e) {
+				if (handler != null)
+					handler.accept((E) e);
+				return value == null ? null : value.get();
+			}
+		};
+	}
+
+	default Supplier withHandler(Consumer super E> handler, T value) {
+		return withHandler(handler, () -> value);
+	}
+
+	default Supplier withHandler(Consumer super E> handler) {
+		return withHandler(handler, (Supplier) null);
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/iterators/ArrayIterator.java b/src/main/java/ru/windcorp/jputil/iterators/ArrayIterator.java
index dc0cdcd..0b8fab2 100644
--- a/src/main/java/ru/windcorp/jputil/iterators/ArrayIterator.java
+++ b/src/main/java/ru/windcorp/jputil/iterators/ArrayIterator.java
@@ -1,47 +1,48 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.iterators;
-
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-
-public class ArrayIterator implements Iterator {
-	
-	private final E[] array;
-	private int next;
-	
-	@SafeVarargs
-	public ArrayIterator(E... array) {
-		this.array = array;
-	}
-
-	@Override
-	public boolean hasNext() {
-		return next < array.length;
-	}
-
-	@Override
-	public E next() {
-		try {
-			return array[next++];
-		} catch (ArrayIndexOutOfBoundsException e) {
-			throw new NoSuchElementException();
-		}
-	}
-
-}
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil.iterators;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+public class ArrayIterator implements Iterator {
+
+	private final E[] array;
+	private int next;
+
+	@SafeVarargs
+	public ArrayIterator(E... array) {
+		this.array = array;
+	}
+
+	@Override
+	public boolean hasNext() {
+		return next < array.length;
+	}
+
+	@Override
+	public E next() {
+		try {
+			return array[next++];
+		} catch (ArrayIndexOutOfBoundsException e) {
+			throw new NoSuchElementException();
+		}
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/iterators/FunctionIterator.java b/src/main/java/ru/windcorp/jputil/iterators/FunctionIterator.java
index a8d63ae..abc5b44 100644
--- a/src/main/java/ru/windcorp/jputil/iterators/FunctionIterator.java
+++ b/src/main/java/ru/windcorp/jputil/iterators/FunctionIterator.java
@@ -1,61 +1,61 @@
-/*******************************************************************************
- * JPUtil
- * Copyright (C) 2019  Javapony/OLEGSHA
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.jputil.iterators;
-
-import java.util.Iterator;
-import java.util.function.Function;
-
-/**
- * @author Javapony
- *
- */
-public class FunctionIterator implements Iterator {
-	
-	private final Iterator parent;
-	private final Function function;
-
-	public FunctionIterator(Iterator