Skip to content

Instantly share code, notes, and snippets.

@antunflas
Last active March 18, 2018 03:47
Show Gist options
  • Save antunflas/26334d6de6a68faed4f57bcc70c61b8f to your computer and use it in GitHub Desktop.
Save antunflas/26334d6de6a68faed4f57bcc70c61b8f to your computer and use it in GitHub Desktop.
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