Skip to content

Commit

Permalink
Add support for empty values in NumberEditor
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrii Chernenko committed Jan 14, 2021
1 parent 98cb201 commit 5fd990e
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 10 deletions.
56 changes: 46 additions & 10 deletions Form/NumberEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,40 @@ public struct NumberEditor<Value> {
private var minFractionDigits: Int // If greater than zero, "cash register" will be used
private var internalText: String // The internal text is just a string of digits [0-9].
private var isNegative: Bool = false
private let allowsEmptyValues: Bool
private let valueToDecimal: (Value) -> NSDecimalNumber
private let decimalToValue: (NSDecimalNumber) -> Value

public var shouldResetOnInsertion: Bool = false

public let defaultValue: Value

/// Creates a new instance with using `formatter` settings for editing.
/// Parameters:
/// - valueToDecimal: How to convert a `Value` to a decimal number.
/// - decimalToValue: How to convert a decimal number back to a `Value`.
public init(formatter: NumberFormatter, valueToDecimal: @escaping (Value) -> NSDecimalNumber, decimalToValue: @escaping (NSDecimalNumber) -> Value) {
public init(
formatter: NumberFormatter,
valueToDecimal: @escaping (Value) -> NSDecimalNumber,
decimalToValue: @escaping (NSDecimalNumber) -> Value,
allowsEmptyValues: Bool = false
) {
let formatter = formatter.copy
formatter.generatesDecimalNumbers = true
minFractionDigits = formatter.minimumFractionDigits
formatterBox = Box(formatter)

self.defaultValue = decimalToValue(0)
self.internalText = "0"
self.defaultValue = decimalToValue(allowsEmptyValues ? .notANumber : 0)
self.internalText = allowsEmptyValues ? "" : "0"

self.valueToDecimal = valueToDecimal
self.decimalToValue = decimalToValue
self.allowsEmptyValues = allowsEmptyValues
}
}

extension NumberEditor: TextEditor {

public var value: Value {
get { return decimalToValue(decimalFromInternalText) }
set { updateInternalText(from: valueToDecimal(newValue)) }
Expand Down Expand Up @@ -104,25 +113,28 @@ extension NumberEditor: TextEditor {
}

public extension NumberEditor where Value == NSDecimalNumber {
init(formatter: NumberFormatter) {
self.init(formatter: formatter, valueToDecimal: { $0 }, decimalToValue: { $0 })
init(formatter: NumberFormatter, allowsEmptyValues: Bool = false) {
self.init(formatter: formatter, valueToDecimal: { $0 }, decimalToValue: { $0 }, allowsEmptyValues: allowsEmptyValues)
}
}

public extension NumberEditor where Value: BinaryInteger {
init(formatter: NumberFormatter = .defaultInteger) {

init(formatter: NumberFormatter = .defaultInteger, allowsEmptyValues: Bool = false) {
precondition(formatter.maximumFractionDigits == 0, "formatter used for integers must have maximumFractionDigits == 0")
self.init(formatter: formatter,
valueToDecimal: { NSDecimalNumber(value: Int64($0)) },
decimalToValue: { Value(truncatingIfNeeded: $0.uint64Value) })
decimalToValue: { Value(truncatingIfNeeded: $0.uint64Value) },
allowsEmptyValues: allowsEmptyValues)
}
}

public extension NumberEditor where Value: BinaryFloatingPoint & CustomStringConvertible {
init(formatter: NumberFormatter = .defaultDecimal) {
init(formatter: NumberFormatter = .defaultDecimal, allowsEmptyValues: Bool = false) {
self.init(formatter: formatter,
valueToDecimal: { NSDecimalNumber(value: Double($0.description) ?? .nan) },
decimalToValue: { Value($0.doubleValue) })
decimalToValue: { Value($0.doubleValue) },
allowsEmptyValues: allowsEmptyValues)
}
}

Expand All @@ -147,13 +159,18 @@ private extension NumberFormatter {
}

private extension NumberEditor {

var decimalFromInternalText: NSDecimalNumber {
let decimal = decimalFromInternalText(internalText)
guard decimal != .negativeZero else { return .zero }
return decimal
}

private func decimalFromInternalText(_ internalText: String) -> NSDecimalNumber {
if internalText.isEmpty {
return .notANumber
}

var textWithDecimal = String(Array(repeating: Character("0"), count: 20)) + internalText
textWithDecimal.insert(".", at: textWithDecimal.index(textWithDecimal.endIndex, offsetBy: -minimumFractionDigits, limitedBy: textWithDecimal.startIndex)!)
let value = NSDecimalNumber(string: textWithDecimal)
Expand All @@ -165,6 +182,15 @@ private extension NumberEditor {
}

mutating func updateInternalText(from value: NSDecimalNumber) {
if value == .notANumber {
internalText = ""
alwaysShowsDecimalSeparator = false
minimumFractionDigits = minFractionDigits
isNegative = false

return
}

let value = formatter.formattedValue(for: value)
var chars = value.stringValue.map { character in character == "." ? decimalCharacter : character }

Expand Down Expand Up @@ -201,8 +227,10 @@ private extension NumberEditor {
mutating func deleteLast() {
let previous = internalText
internalText = String(internalText.dropLast())

if internalText.isEmpty { // `-3` -> `-0` and `-0` -> `0`
internalText = "0"
internalText = allowsEmptyValues ? "" : "0"

if previous == "0" {
isNegative = false
}
Expand All @@ -211,17 +239,25 @@ private extension NumberEditor {

private func isInternalTextValid(_ internalText: String) -> Bool {
let value = decimalFromInternalText(internalText)

if value == .notANumber && allowsEmptyValues {
return true
}

if let maximum = formatter.maximum, value > NSDecimalNumber(value: maximum.doubleValue) {
return false
}

if let minimum = formatter.minimum, value < NSDecimalNumber(value: minimum.doubleValue) {
return false
}

return true
}
}

private extension NumberEditor {

var alwaysShowsDecimalSeparator: Bool {
get { return formatter.alwaysShowsDecimalSeparator }
set { mutatingFormatter.alwaysShowsDecimalSeparator = newValue }
Expand Down
14 changes: 14 additions & 0 deletions FormTests/NumberEditorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class DecimalEditorTests: XCTestCase {

var decimalFormatter: NumberFormatter {
let formatter = NumberFormatter()
formatter.notANumberSymbol = ""
formatter.generatesDecimalNumbers = true
formatter.numberStyle = .decimal
formatter.decimalSeparator = "."
Expand Down Expand Up @@ -256,4 +257,17 @@ class DecimalEditorTests: XCTestCase {
test(editor, "123456R3", "3", 3, 0)
test(editor, "12345.6788R9", "9", 9, 0)
}

func testEditing_whenEmptyValuesAreAllowed() {
let formatter = decimalFormatter
let editor = NumberEditor(formatter: formatter, allowsEmptyValues: true)

test(editor, "111<<<", "", .notANumber, 0)
test(editor, "111<<<<", "", .notANumber, 0)
test(editor, "111<<<0022.1", "22.1", 22.1, 0)
test(editor, "111<<<-22.1", "-22.1", -22.1, 0)
test(editor, "111r", "", .notANumber, 0)
test(editor, "-111r", "", .notANumber, 0)
test(editor, "111R234", "234", 234, 0)
}
}

0 comments on commit 5fd990e

Please sign in to comment.