Last active
March 18, 2018 03:47
-
-
Save antunflas/26334d6de6a68faed4f57bcc70c61b8f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.text.SpannableStringBuilder; | |
import android.text.Spanned; | |
import android.util.SparseArray; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.IllegalFormatPrecisionException; | |
import java.util.List; | |
import java.util.UnknownFormatConversionException; | |
public class Basket { | |
private SparseArray<Argument> args; | |
private int index; | |
private List<Token> tokens; | |
private CharSequence format; | |
public Basket(CharSequence format) { | |
this.args = new SparseArray<>(); | |
this.index = 0; | |
this.tokens = tokenize(format); | |
this.format = format; | |
} | |
public Basket add(Object value, Object... spans) { | |
return put(index++, value, spans); | |
} | |
public Basket put(int i, Object value, Object... spans) { | |
this.args.put(i, createArgument(value, spans)); | |
return this; | |
} | |
public CharSequence grab() { | |
SpannableStringBuilder builder = new SpannableStringBuilder(); | |
List<Span> spans = spans(format); | |
int pos = 0; | |
for (Token token : tokens) { | |
// format token with arguments and get diff between unformatted length and formatted length | |
int diff = token.consumed - token.resolve(builder, args); | |
// adjust span bounds according to diff | |
for (int i = 0; i < spans.size(); i++) { | |
adjustSpan(spans.get(i), pos, diff); | |
} | |
pos += token.consumed; | |
} | |
span(builder, spans); | |
style(builder); | |
return builder; | |
} | |
private Argument createArgument(Object value, Object... spans) { | |
Object[] spanArray; | |
if (value instanceof Spanned) { | |
Spanned spanned = (Spanned) value; | |
Object[] originSpans = spanned.getSpans(0, spanned.length(), Object.class); | |
spanArray = Arrays.copyOf(spans, spans.length + originSpans.length); | |
System.arraycopy(originSpans, 0, spanArray, spans.length, originSpans.length); | |
} else { | |
spanArray = spans; | |
} | |
return new Argument(value, spanArray); | |
} | |
/** | |
* Adjust span according to diff between unformatted string length and formatted string length. | |
* There are three possibilities __1__|__2__|__3__ ; | = span bound | |
* 1. position where diff happened is before span => move span's both start and end forward for diff | |
* 2. position where diff happened is inside span => move just span's end forward for diff | |
* 3. position where diff happened is after span => nothing to adjust, span is on good position | |
* | |
* @param span Span holder | |
* @param pos Position in original format string | |
* @param diff Diff between original format string length and formatted string length | |
*/ | |
private void adjustSpan(Span span, int pos, int diff) { | |
// this span didn't start yet - if there was a diff move it forward | |
if (pos < span.originalStart) { | |
span.start -= diff; | |
} | |
// this span didn't end yet - if there was a diff move end forward | |
if (pos < span.originalEnd) { | |
span.end -= Math.min(diff, span.originalEnd - pos); | |
} | |
} | |
private void span(SpannableStringBuilder builder, List<Span> spans) { | |
for (Span span : spans) { | |
builder.setSpan(span.span, span.start, span.end, span.flags); | |
} | |
} | |
private void style(SpannableStringBuilder builder) { | |
for (Token token : tokens) { | |
token.style(builder, args); | |
} | |
} | |
private List<Token> tokenize(CharSequence sequence) { | |
List<Token> tokens = new ArrayList<>(); | |
String string = sequence.toString(); | |
int implicit = 0; | |
for (int i = 0; i < string.length();) { | |
int nextPercent = string.indexOf('%', i); | |
if (string.charAt(i) != '%') { | |
int start = i; | |
int end = nextPercent == -1 ? string.length() : nextPercent; | |
tokens.add(new TextToken(sequence.subSequence(start, end), end - start)); | |
i = end; | |
} else { | |
FormatSpecifierParser fsp = new FormatSpecifierParser(string.substring(i + 1)); | |
int index = 0; | |
if (fsp.getIndex() > 0 && implicit <= 0) { | |
index = fsp.getIndex(); | |
implicit = -1; | |
} else if (implicit >= 0) { | |
index = ++implicit; | |
} else { | |
//TODO: there is no index defined and we are using explicit indexing: throw exception? | |
} | |
tokens.add(new PlaceholderToken('%' + fsp.getFormat(), fsp.getConsumed() + 1, index)); | |
i += fsp.getConsumed() + 1; | |
} | |
} | |
return tokens; | |
} | |
private List<Span> spans(CharSequence cs) { | |
List<Span> list = new ArrayList<>(); | |
if (cs instanceof Spanned) { | |
Spanned s = (Spanned) cs; | |
Object[] spans = s.getSpans(0, cs.length(), Object.class); | |
for (Object span : spans) { | |
int end = s.getSpanEnd(span); | |
list.add(new Span(span, s.getSpanStart(span), cs.length() < end ? cs.length() : end, s.getSpanFlags(span))); | |
} | |
} | |
return list; | |
} | |
private static class Span { | |
private Object span; | |
private int originalStart; | |
private int originalEnd; | |
private int start; | |
private int end; | |
private int flags; | |
public Span(Object span, int start, int end, int flags) { | |
this.span = span; | |
this.start = start; | |
this.end = end; | |
this.flags = flags; | |
this.originalStart = start; | |
this.originalEnd = end; | |
} | |
} | |
private abstract static class Token { | |
final CharSequence value; | |
final int consumed; | |
Token(CharSequence value, int consumed) { | |
this.value = value; | |
this.consumed = consumed; | |
} | |
abstract int resolve(SpannableStringBuilder builder, SparseArray<Argument> args); | |
abstract void style(SpannableStringBuilder builder, SparseArray<Argument> args); | |
@Override | |
public String toString() { | |
return "Token{" | |
+ "value='" + value | |
+ "'}"; | |
} | |
} | |
private static class PlaceholderToken extends Token { | |
private int index; | |
private int start; | |
private int end; | |
PlaceholderToken(CharSequence value, int consumed, int index) { | |
super(value, consumed); | |
this.index = index; | |
} | |
@Override | |
int resolve(SpannableStringBuilder builder, SparseArray<Argument> args) { | |
Argument argument = args.get(index == 0 ? 0 : index - 1); | |
start = builder.length(); | |
if (argument == null) { | |
// IMPORTANT: do not remove this String.format call - you will break correct formatting of %% | |
builder.append(String.format(value.toString())); | |
} else { | |
builder.append(String.format(value.toString(), argument.value)); | |
} | |
end = builder.length(); | |
return end - start; | |
} | |
@Override | |
void style(SpannableStringBuilder builder, SparseArray<Argument> args) { | |
Argument argument = args.get(index == 0 ? 0 : index - 1); | |
if (argument != null) { | |
for (int i = 0; i < argument.spans.length; i++) { | |
builder.setSpan(argument.spans[i], start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); | |
} | |
} | |
} | |
@Override | |
public String toString() { | |
return "PlaceholderToken{" | |
+ "index=" + index | |
+ ", " + super.toString() | |
+ '}'; | |
} | |
} | |
private static class TextToken extends Token { | |
TextToken(CharSequence value, int consumed) { | |
super(value, consumed); | |
} | |
@Override | |
int resolve(SpannableStringBuilder builder, SparseArray<Argument> args) { | |
int start = builder.length(); | |
builder.append(value); | |
return builder.length() - start; | |
} | |
@Override | |
void style(SpannableStringBuilder builder, SparseArray<Argument> args) { | |
} | |
} | |
private static final class Argument { | |
final Object value; | |
final Object[] spans; | |
Argument(Object value, Object[] spans) { | |
this.value = value; | |
this.spans = spans; | |
} | |
} | |
/** | |
* Parses the format specifier. | |
* %[argument_index$][flags][width][.precision][t]conversion | |
*/ | |
private static class FormatSpecifierParser { | |
private String format; | |
private int cursor; | |
private String index; | |
private String flags; | |
private String width; | |
private String precision; | |
private String tT; | |
private String conv; | |
private int consumed; | |
private static final String FLAGS = ",-(+# 0<"; | |
public FormatSpecifierParser(String format) { | |
this.format = format; | |
int startIdx = cursor = 0; | |
// Index | |
if (nextIsInt()) { | |
String nint = nextInt(); | |
if (peek() == '$') { | |
index = nint; | |
this.format = format.substring(cursor + 1); | |
cursor = startIdx; | |
consumed++; | |
} else if (nint.charAt(0) == '0') { | |
// This is a flag, skip to parsing flags. | |
back(nint.length()); | |
} else { | |
// This is the width, skip to parsing precision. | |
width = nint; | |
} | |
} | |
// Flags | |
flags = ""; | |
while (width == null && FLAGS.indexOf(peek()) >= 0) { | |
flags += advance(); | |
} | |
// Width | |
if (width == null && nextIsInt()) { | |
width = nextInt(); | |
} | |
// Precision | |
if (peek() == '.') { | |
advance(); | |
if (!nextIsInt()) { | |
throw new IllegalFormatPrecisionException(peek()); | |
} | |
precision = nextInt(); | |
} | |
// tT | |
if (peek() == 't' || peek() == 'T') { | |
tT = String.valueOf(advance()); | |
} | |
// Conversion | |
conv = String.valueOf(advance()); | |
this.format = this.format.substring(0, cursor); | |
} | |
private String nextInt() { | |
int strBegin = cursor; | |
while (nextIsInt()) { | |
advance(); | |
} | |
return format.substring(strBegin, cursor); | |
} | |
private boolean nextIsInt() { | |
return !isEnd() && Character.isDigit(peek()); | |
} | |
private char peek() { | |
if (isEnd()) { | |
throw new UnknownFormatConversionException("End of String"); | |
} | |
return format.charAt(cursor); | |
} | |
private char advance() { | |
if (isEnd()) { | |
throw new UnknownFormatConversionException("End of String"); | |
} | |
consumed++; | |
return format.charAt(cursor++); | |
} | |
private void back(int len) { | |
cursor -= len; | |
} | |
private boolean isEnd() { | |
return cursor == format.length(); | |
} | |
public int getIndex() { | |
if (index != null) { | |
return Integer.parseInt(index); | |
} else { | |
return 0; | |
} | |
} | |
public String getFormat() { | |
return format; | |
} | |
public int getConsumed() { | |
return consumed; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment