Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Sources/ShellSpectroscopy/EDX/EDXFactorsSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,28 @@ 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)
}
}
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)
}
Expand Down
18 changes: 18 additions & 0 deletions Sources/ShellSpectroscopy/EDX/EDXSheetNumber.swift
Original file line number Diff line number Diff line change
@@ -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<Int>, locale: Locale = .autoupdatingCurrent) -> DecimalEntryFormat<FloatingPointFormatStyle<Double>> {
DecimalEntryFormat(FloatingPointFormatStyle<Double>.number.precision(.significantDigits(digits)).locale(locale), locale: locale)
}
}
4 changes: 2 additions & 2 deletions Sources/ShellSpectroscopy/EDX/EDXTypedKSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
51 changes: 51 additions & 0 deletions Tests/ShellSpectroscopyTests/EDXSheetNumberTests.swift
Original file line number Diff line number Diff line change
@@ -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<Int> = 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<Double>.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))
}
}
}
Loading