From ce46fbf6247a52a428ec63a511a907343783993d Mon Sep 17 00:00:00 2001 From: Paullo9 Date: Thu, 5 Nov 2026 13:19:25 +0100 Subject: [PATCH] fix: the EDS typed-k and factors sheets read either decimal separator On a German-locale Mac the typed-k sheet read "1.9476" as 19 476 300 000 000: its fields used SwiftUI's bare .number format, whose parse takes a period for grouping in a comma region, and Quantify ran with it. The six fields of the typed-k and zeta / cross-section sheets now parse through DecimalEntryFormat, the rule the shared numeric fields already use: one . or , is the decimal point, well-formed grouping is grouping, anything else is refused. Fixes #71. Done: the sheets read 1.9476 and 1,9476 alike. Next: the typed-k sheet's container identifier (separate issue). Co-Authored-By: Claude Opus 5.5 --- .../EDX/EDXFactorsSheet.swift | 8 +-- .../EDX/EDXSheetNumber.swift | 18 +++++++ .../EDX/EDXTypedKSheet.swift | 4 +- .../EDXSheetNumberTests.swift | 51 +++++++++++++++++++ 4 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 Sources/ShellSpectroscopy/EDX/EDXSheetNumber.swift create mode 100644 Tests/ShellSpectroscopyTests/EDXSheetNumberTests.swift diff --git a/Sources/ShellSpectroscopy/EDX/EDXFactorsSheet.swift b/Sources/ShellSpectroscopy/EDX/EDXFactorsSheet.swift index 3bba86f..3c370c1 100644 --- a/Sources/ShellSpectroscopy/EDX/EDXFactorsSheet.swift +++ b/Sources/ShellSpectroscopy/EDX/EDXFactorsSheet.swift @@ -37,7 +37,7 @@ struct EDXFactorsSheet: View { ForEach($draft.rows) { $row in GridRow { Text(row.element).font(.body.weight(.semibold)) - TextField(method.factorName, value: $row.value, format: .number.precision(.significantDigits(1...6)), prompt: Text("required")) + TextField(method.factorName, value: $row.value, format: EDXSheetNumber.format(digits: 1...6), prompt: Text("required")) .labelsHidden().multilineTextAlignment(.trailing).frame(width: EDXLayout.factorFieldWidth) .accessibilityLabel("\(method.factorName) of \(row.element)") Text(unit).font(.caption).foregroundStyle(.secondary) @@ -45,20 +45,20 @@ struct EDXFactorsSheet: View { } GridRow { Text("Beam current") - TextField("Beam current", value: $draft.beamCurrentNA, format: .number.precision(.significantDigits(1...6)), prompt: Text("required")) + TextField("Beam current", value: $draft.beamCurrentNA, format: EDXSheetNumber.format(digits: 1...6), prompt: Text("required")) .labelsHidden().multilineTextAlignment(.trailing).frame(width: EDXLayout.factorFieldWidth) Text("nA").font(.caption).foregroundStyle(.secondary) } GridRow { Text("Live time") - TextField("Live time", value: $draft.liveTimeSeconds, format: .number.precision(.significantDigits(1...6)), prompt: Text("required")) + TextField("Live time", value: $draft.liveTimeSeconds, format: EDXSheetNumber.format(digits: 1...6), prompt: Text("required")) .labelsHidden().multilineTextAlignment(.trailing).frame(width: EDXLayout.factorFieldWidth) Text("s per pixel").font(.caption).foregroundStyle(.secondary) } if method == .crossSection { GridRow { Text("Probe area") - TextField("Probe area", value: $draft.probeAreaNm2, format: .number.precision(.significantDigits(1...6)), prompt: Text("required")) + TextField("Probe area", value: $draft.probeAreaNm2, format: EDXSheetNumber.format(digits: 1...6), prompt: Text("required")) .labelsHidden().multilineTextAlignment(.trailing).frame(width: EDXLayout.factorFieldWidth) Text("nm²").font(.caption).foregroundStyle(.secondary) } diff --git a/Sources/ShellSpectroscopy/EDX/EDXSheetNumber.swift b/Sources/ShellSpectroscopy/EDX/EDXSheetNumber.swift new file mode 100644 index 0000000..8deffb5 --- /dev/null +++ b/Sources/ShellSpectroscopy/EDX/EDXSheetNumber.swift @@ -0,0 +1,18 @@ +// +// EDXSheetNumber.swift +// Role: the number format of the EDS sheets' typed fields (k-factors, zeta / cross-section factors, dose). SwiftUI's own `.number` +// parse, in a region whose decimal separator is a comma, reads a typed period as grouping: "1.9476" became 19 476 300 000 000 +// in the typed-k sheet on a German Mac, with no warning (MacEMUI issue, 2026-11-05). These fields now parse through +// `DecimalEntryFormat`, the rule every inspector number already uses: one `.` or `,` is the decimal point, well-formed +// grouping is grouping, anything else is refused (the field keeps its last value). +// + +import Foundation +import ShellControls + +nonisolated enum EDXSheetNumber { + /// A decimal field with `digits` significant digits, either separator accepted. + static func format(digits: ClosedRange, locale: Locale = .autoupdatingCurrent) -> DecimalEntryFormat> { + DecimalEntryFormat(FloatingPointFormatStyle.number.precision(.significantDigits(digits)).locale(locale), locale: locale) + } +} diff --git a/Sources/ShellSpectroscopy/EDX/EDXTypedKSheet.swift b/Sources/ShellSpectroscopy/EDX/EDXTypedKSheet.swift index e8588ed..18c97c7 100644 --- a/Sources/ShellSpectroscopy/EDX/EDXTypedKSheet.swift +++ b/Sources/ShellSpectroscopy/EDX/EDXTypedKSheet.swift @@ -36,10 +36,10 @@ struct EDXTypedKSheet: View { ForEach($draft.rows) { $row in GridRow { Text(row.element).font(.body.weight(.semibold)) - TextField("k", value: $row.k, format: .number.precision(.significantDigits(1...6)), prompt: Text("required")) + TextField("k", value: $row.k, format: EDXSheetNumber.format(digits: 1...6), prompt: Text("required")) .labelsHidden().multilineTextAlignment(.trailing).frame(width: EDXLayout.kFieldWidth) .accessibilityLabel("k of \(row.element)") - TextField("σ", value: $row.sigmaPercent, format: .number.precision(.significantDigits(1...4)), prompt: Text("default")) + TextField("σ", value: $row.sigmaPercent, format: EDXSheetNumber.format(digits: 1...4), prompt: Text("default")) .labelsHidden().multilineTextAlignment(.trailing).frame(width: EDXLayout.sigmaFieldWidth) .accessibilityLabel("σ of \(row.element) in percent") } diff --git a/Tests/ShellSpectroscopyTests/EDXSheetNumberTests.swift b/Tests/ShellSpectroscopyTests/EDXSheetNumberTests.swift new file mode 100644 index 0000000..9c99b7a --- /dev/null +++ b/Tests/ShellSpectroscopyTests/EDXSheetNumberTests.swift @@ -0,0 +1,51 @@ +// +// EDXSheetNumberTests.swift +// 2026-11-05, on a German-locale Mac: the typed-k sheet read "1.9476" as 19 476 300 000 000 (SwiftUI's `.number` parse takes the +// period for grouping) and quantified with it, with no warning. The EDS sheets' fields now parse through `DecimalEntryFormat`. +// + +import Foundation +import XCTest +import ShellControls +@testable import ShellSpectroscopy + +nonisolated final class EDXSheetNumberTests: XCTestCase { + private let german = Locale(identifier: "de_DE") + private let american = Locale(identifier: "en_US") + + private func parse(_ text: String, _ locale: Locale, digits: ClosedRange = 1...6) throws -> Double { + try EDXSheetNumber.format(digits: digits, locale: locale).parseStrategy.parse(text) + } + + /// The reproduction: the format the sheets used before, under de_DE, does not read "1.9476" as 1.9476. + func testTheOldFormatMisreadsAPeriodUnderGerman() throws { + let old = FloatingPointFormatStyle.number.precision(.significantDigits(1...6)).locale(german) + XCTAssertNotEqual((try? old.parseStrategy.parse("1.9476")) ?? .nan, 1.9476, accuracy: 1e-9) + } + + /// Either separator is the decimal point under de_DE and en_US: the typed k-factors of the Al-Mg-Si demo. + func testBothSeparatorsReadAsTheDecimalPoint() throws { + for locale in [german, american] { + XCTAssertEqual(try parse("1.9476", locale), 1.9476, accuracy: 1e-12, "\(locale.identifier)") + XCTAssertEqual(try parse("1,9476", locale), 1.9476, accuracy: 1e-12, "\(locale.identifier)") + XCTAssertEqual(try parse("1.16691", locale), 1.16691, accuracy: 1e-12, "\(locale.identifier)") + XCTAssertEqual(try parse("1", locale), 1, "\(locale.identifier)") + XCTAssertEqual(try parse("20", locale, digits: 1...4), 20, "\(locale.identifier)") + } + } + + /// Text with no single reading is refused, never turned into another number. + func testAmbiguousTextIsRefused() { + XCTAssertThrowsError(try parse("1.9.476", german)) + XCTAssertThrowsError(try parse("1,94,76", german)) + XCTAssertThrowsError(try parse("19.4.", german)) + } + + /// The shown value carries no grouping, so a value committed unchanged reads back as itself. + func testTheShownValueReadsBackAsItself() throws { + let f = EDXSheetNumber.format(digits: 1...6, locale: german) + for v in [1.9476, 1234.5, 0.00275] { + XCTAssertEqual(try f.parseStrategy.parse(f.format(v)), v, accuracy: 1e-9 * max(1, v)) + } + } +}