Java tutorial
/* * Copyright 2016 Google Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.blockly.model; import android.text.TextUtils; import android.util.Log; import com.google.blockly.android.control.BlocklyEvent.ChangeEvent; import com.google.blockly.utils.BlockLoadingException; import org.json.JSONException; import org.json.JSONObject; import java.text.DecimalFormat; import java.util.Arrays; /** * A 'field_number' type of field, for an editable number. */ public final class FieldNumber extends Field { private static final String TAG = "FieldNumber"; public static final double NO_CONSTRAINT = Double.NaN; /** * This formatter is used by fields without precision, and to count precision's significant * digits past the decimal point. Unlike {@link Double#toString}, it displays as many * fractional digits as possible. */ private static final DecimalFormat NAIVE_DECIMAL_FORMAT; static { char[] sigDigts = new char[100]; Arrays.fill(sigDigts, '#'); NAIVE_DECIMAL_FORMAT = new DecimalFormat(new StringBuffer("0.").append(sigDigts).toString()); } /** * This formatter is used when precision is a multiple of 1. */ protected static final DecimalFormat INTEGER_DECIMAL_FORMAT = new DecimalFormat("0"); private double mValue; private double mMin = NO_CONSTRAINT; private double mMax = NO_CONSTRAINT; private double mPrecision = NO_CONSTRAINT; private DecimalFormat mFormatter; private boolean mIntegerPrecision; private double mEffectiveMin = -Double.MAX_VALUE; // mMin as a multiple of mPrecision private double mEffectiveMax = Double.MAX_VALUE; // mMax as a multiple of mPrecision public FieldNumber(String name) { super(name, TYPE_NUMBER); } public static FieldNumber fromJson(JSONObject json) throws BlockLoadingException { String name = json.optString("name", null); if (name == null) { throw new BlockLoadingException("Number fields must have name field."); } FieldNumber field = new FieldNumber(name); if (json.has("value")) { try { field.setValue(json.getDouble("value")); } catch (JSONException e) { throw new BlockLoadingException( "Cannot parse field_number value: " + json.optString("value", "[object or array]")); } } try { field.setConstraints(json.optDouble("min", NO_CONSTRAINT), json.optDouble("max", NO_CONSTRAINT), json.optDouble("precision", NO_CONSTRAINT)); } catch (IllegalArgumentException e) { throw new BlockLoadingException(e); } return field; } @Override public FieldNumber clone() { FieldNumber copy = new FieldNumber(getName()); copy.setValue(mValue); copy.setConstraints(mMin, mMax, mPrecision); return copy; } /** * Sets the constraints on valid number values. * <p/> * Changing the constraints may trigger a {@link ChangeEvent}, even if the value does not * change. * * @param min The minimum allowed value, inclusive. * @param max The maximum allowed value, inclusive. * @param precision The precision of allowed values. Valid values are multiples of this number, * such as 1, 0.1, 100, or 0.125. */ public void setConstraints(double min, double max, double precision) { if (max == Double.POSITIVE_INFINITY || Double.isNaN(max)) { max = NO_CONSTRAINT; } else if (max == Double.NEGATIVE_INFINITY) { throw new IllegalArgumentException("Max cannot be -Inf. No valid values would exist."); } if (min == Double.NEGATIVE_INFINITY || Double.isNaN(min)) { min = NO_CONSTRAINT; } else if (min == Double.POSITIVE_INFINITY) { throw new IllegalArgumentException("Min cannot be Inf. No valid values would exist."); } if (precision == 0 || Double.isNaN(precision)) { precision = NO_CONSTRAINT; } if (Double.isInfinite(precision)) { throw new IllegalArgumentException("Precision cannot be infinite."); } if (!Double.isNaN(min) && !Double.isNaN(max) && min > max) { throw new IllegalArgumentException("Minimum value must be less than max. Found " + min + " > " + max); } if (!Double.isNaN(precision) && precision <= 0) { throw new IllegalArgumentException("Precision must be positive. Found " + precision); } double effectiveMin = Double.isNaN(min) ? -Double.MAX_VALUE : min; double effectiveMax = Double.isNaN(max) ? Double.MAX_VALUE : max; if (!Double.isNaN(precision)) { if (effectiveMin < 0) { double multiplier = Math.floor(-effectiveMin / precision); effectiveMin = precision * -multiplier; } else { double multiplier = Math.ceil(effectiveMin / precision); effectiveMin = precision * multiplier; } if (effectiveMax < 0) { double multiplier = Math.ceil(-effectiveMax / precision); effectiveMax = precision * -multiplier; } else { double multiplier = Math.floor(effectiveMax / precision); effectiveMax = precision * multiplier; } if (effectiveMin > effectiveMax) { throw new IllegalArgumentException("No valid value in range."); } } mMin = min; mMax = max; mPrecision = precision; mEffectiveMin = effectiveMin; mEffectiveMax = effectiveMax; mIntegerPrecision = (precision == Math.round(precision)); if (!hasPrecision()) { mFormatter = NAIVE_DECIMAL_FORMAT; } else if (mIntegerPrecision) { mFormatter = INTEGER_DECIMAL_FORMAT; } else { String precisionStr = NAIVE_DECIMAL_FORMAT.format(precision); int decimalChar = precisionStr.indexOf('.'); if (decimalChar == -1) { mFormatter = INTEGER_DECIMAL_FORMAT; } else { int significantDigits = precisionStr.length() - decimalChar; StringBuilder sb = new StringBuilder("0."); char[] sigDigitsFormat = new char[significantDigits]; Arrays.fill(sigDigitsFormat, '#'); sb.append(sigDigitsFormat); mFormatter = new DecimalFormat(sb.toString()); } } setValueImpl(mValue, true); } /** * Sets the value from a string. As long as the text can be parsed as a number, the value will * be accepted. The actual value assigned might be modified to fit the min, max, and precision * constraints. * * @param text The text value for this field. * * @return True if the value parsed without error and the value has been updated. */ @Override public boolean setFromString(String text) { if (TextUtils.isEmpty(text)) { Log.e(TAG, "text was empty" + (text == null ? "(null)" : "")); return false; } try { double value = Double.parseDouble(text); if (Double.isNaN(value)) { Log.e(TAG, "Value cannot be NaN"); return false; } setValue(value); return true; } catch (NumberFormatException e) { Log.e(TAG, "Not a number: \"" + text + "\""); return false; } } /** * @return The number the user has entered. */ public double getValue() { return mValue; } /** * @return The formatted (human readable) string version of the input. */ public CharSequence getFormattedValue() { return mFormatter.format(mValue); } /** * Sets the number in this Field. The resulting value of this field may differ from * {@code newValue} to adapt to the assigned min, max, and precision constraints. * * @param newValue The number to replace the field content with. */ public void setValue(double newValue) { setValueImpl(newValue, false); } private void setValueImpl(double newValue, boolean onConstraintsChanged) { if (hasPrecision()) { newValue = mPrecision * Math.round(newValue / mPrecision); // Run the value through formatter to limit significant digits. String formattedValue = mFormatter.format(newValue); newValue = Double.parseDouble(formattedValue); } if (hasMinimum() && newValue < mEffectiveMin) { newValue = mEffectiveMin; } else if (hasMaximum() && newValue > mEffectiveMax) { newValue = mEffectiveMax; } if (newValue != mValue || onConstraintsChanged) { String oldStrValue = getSerializedValue(); mValue = newValue; String newStrValue = getSerializedValue(); fireValueChanged(oldStrValue, newStrValue); } } @Override public String getSerializedValue() { if (mValue % 1.0 == 0.0) { // Don't render the decimal point. return INTEGER_DECIMAL_FORMAT.format(mValue); } else { // Render as many decimal places as necessary. Don't abbreviate. return NAIVE_DECIMAL_FORMAT.format(mValue); } } /** * @return True if there's a minimum constraint, false if the minimum is unbounded. */ public boolean hasMinimum() { return !Double.isNaN(mMin); } /** @return The minimum allowed value for this field. */ public double getMinimumValue() { return mMin; } /** * @return True if there's a maximum constraint, false if the maximum is unbounded. */ public boolean hasMaximum() { return !Double.isNaN(mMax); } /** @return The maximum allowed value for this field. */ public double getMaximumValue() { return mMax; } /** * @return True if there's a precision applied to the value, false otherwise. */ public boolean hasPrecision() { return !Double.isNaN(mPrecision); } /** * This returns the precision of the value allowed by this field. The value must be a multiple * of precision. Precision is usually expressed as a power of 10 (e.g., 1, 100, 0.01), though * other useful examples might be 5, 20, or 25. * * @return The precision allowed for the value. */ public double getPrecision() { return mPrecision; } /** @return Whether the precision (and thus the value) is an integer. */ public boolean isInteger() { return mIntegerPrecision; } }