Number formatting settings, recipe ingredient amount calculation

This commit is contained in:
VincentMeilinger
2024-03-20 08:53:58 +01:00
parent 054e222d8e
commit 7b59c79222
10 changed files with 211 additions and 39 deletions

View File

@@ -0,0 +1,56 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC9",
"green" : "0x82",
"red" : "0x00"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "light"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC9",
"green" : "0x82",
"red" : "0x00"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC9",
"green" : "0x82",
"red" : "0x00"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -29,6 +29,8 @@ class ObservableRecipeDetail: ObservableObject {
// Additional functionality
@Published var ingredientMultiplier: Double
init() {
id = ""
name = String(localized: "New Recipe")
@@ -92,9 +94,34 @@ class ObservableRecipeDetail: ObservableObject {
}
static func adjustIngredient(_ ingredient: String, by factor: Double) -> AttributedString {
var matches = ObservableRecipeDetail.matchPatternAndMultiply(.mixedFraction, in: ingredient, multFactor: factor)
matches.append(contentsOf: ObservableRecipeDetail.matchPatternAndMultiply(.fraction, in: ingredient, multFactor: factor, excludedRanges: matches.map({ tuple in tuple.1 })))
matches.append(contentsOf: ObservableRecipeDetail.matchPatternAndMultiply(.number, in: ingredient, multFactor: factor, excludedRanges: matches.map({ tuple in tuple.1 })))
if factor == 0 {
return AttributedString(ingredient)
}
// Match mixed fractions first
var matches = ObservableRecipeDetail.matchPatternAndMultiply(
.mixedFraction,
in: ingredient,
multFactor: factor
)
// Then match fractions, exclude mixed fraction ranges
matches.append(contentsOf:
ObservableRecipeDetail.matchPatternAndMultiply(
.fraction,
in: ingredient,
multFactor: factor,
excludedRanges: matches.map({ tuple in tuple.1 })
)
)
// Match numbers at last, exclude all prior matches
matches.append(contentsOf:
ObservableRecipeDetail.matchPatternAndMultiply(
.number,
in: ingredient,
multFactor: factor,
excludedRanges: matches.map({ tuple in tuple.1 })
)
)
// Sort matches by match range lower bound, descending.
matches.sort(by: { a, b in a.1.lowerBound > b.1.lowerBound})
var attributedString = AttributedString(ingredient)
@@ -102,7 +129,8 @@ class ObservableRecipeDetail: ObservableObject {
print(newSubstring, matchRange)
guard let range = Range(matchRange, in: attributedString) else { continue }
var attributedSubString = AttributedString(newSubstring)
attributedSubString.foregroundColor = .blue
//attributedSubString.foregroundColor = .ncTextHighlight
attributedSubString.font = .system(.body, weight: .bold)
attributedString.replaceSubrange(range, with: attributedSubString)
print("\n", attributedString)
}
@@ -130,8 +158,8 @@ class ObservableRecipeDetail: ObservableObject {
var adjustedValue: Double = 0
switch expr {
case .number:
guard let number = Double(matchedString) else { continue }
adjustedValue = number
guard let number = numberFormatter.number(from: matchedString) else { continue }
adjustedValue = number.doubleValue
case .fraction:
let fracComponents = matchedString.split(separator: "/")
guard fracComponents.count == 2 else { continue }
@@ -160,6 +188,9 @@ class ObservableRecipeDetail: ObservableObject {
}
static func formatNumber(_ value: Double) -> String {
if value <= 0.0001 {
return "0"
}
let integerPart = value >= 1 ? Int(value) : 0
let decimalPart = value - Double(integerPart)
@@ -183,7 +214,7 @@ class ObservableRecipeDetail: ObservableObject {
return "\(String(integerPart)) \(closest.fraction)"
} else {
// If no close match is found, return the original value as a string
return String(format: "%.2f", value)
return numberFormatter.string(from: NSNumber(value: value)) ?? "0"//String(format: "%.2f", value)
}
}
@@ -192,17 +223,30 @@ class ObservableRecipeDetail: ObservableObject {
}
}
enum RegexPattern {
enum RegexPattern: String, CaseIterable, Identifiable {
case mixedFraction, fraction, number
var id: String { self.rawValue }
var pattern: String {
switch self {
case .mixedFraction:
#"(\d+)\s+(\d+)/(\d+)"#
case .fraction:
#"(?:[1-9][0-9]*|0)\/[1-9][0-9]*"#
#"(?:[1-9][0-9]*|0)\/([1-9][0-9]*)"#
case .number:
#"(\d+(\.\d+)?)"#
#"(\d+([.,]\d+)?)"#
}
}
var localizedDescription: LocalizedStringKey {
switch self {
case .mixedFraction:
"Mixed fraction"
case .fraction:
"Fraction"
case .number:
"Number"
}
}
}

View File

@@ -115,6 +115,12 @@ class UserSettings: ObservableObject {
}
}
@Published var decimalNumberSeparator: String {
didSet {
UserDefaults.standard.set(decimalNumberSeparator, forKey: "decimalNumberSeparator")
}
}
init() {
self.username = UserDefaults.standard.object(forKey: "username") as? String ?? ""
self.token = UserDefaults.standard.object(forKey: "token") as? String ?? ""
@@ -133,6 +139,7 @@ class UserSettings: ObservableObject {
self.expandKeywordSection = UserDefaults.standard.object(forKey: "expandKeywordSection") as? Bool ?? false
self.expandInfoSection = UserDefaults.standard.object(forKey: "expandInfoSection") as? Bool ?? false
self.keepScreenAwake = UserDefaults.standard.object(forKey: "keepScreenAwake") as? Bool ?? true
self.decimalNumberSeparator = UserDefaults.standard.object(forKey: "decimalNumberSeparator") as? String ?? "."
if authString == "" {
if token != "" && username != "" {

View File

@@ -27,4 +27,7 @@ extension Color {
public static var ncGradientLight: Color {
return Color("ncgradientlightblue")
}
public static var ncTextHighlight: Color {
return Color("textHighlight")
}
}

View File

@@ -354,6 +354,12 @@
}
}
}
},
"1,42 (Comma)" : {
},
"1.42 (Point)" : {
},
"A recipe with that name already exists." : {
"localizations" : {
@@ -1094,6 +1100,12 @@
},
"decilitre" : {
},
"Decimal number format" : {
},
"Decimal Separator" : {
},
"Delete" : {
"localizations" : {
@@ -1575,6 +1587,9 @@
},
"fluid ounce" : {
},
"Fraction" : {
},
"g" : {
@@ -2309,6 +2324,9 @@
}
}
}
},
"Mixed fraction" : {
},
"ml" : {
@@ -2489,6 +2507,9 @@
}
}
}
},
"Number" : {
},
"Nutrition" : {
"localizations" : {
@@ -2577,9 +2598,6 @@
}
}
}
},
"Only highlighted ingredient were adjusted!" : {
},
"Other" : {
"localizations" : {
@@ -3682,6 +3700,9 @@
}
}
}
},
"This setting will take effect after the app is restarted. It affects the adjustment of ingredient quantities." : {
},
"This website might not be currently supported. If this appears incorrect, you can use the support options in the app settings to raise awareness about this issue." : {
"localizations" : {
@@ -3864,6 +3885,9 @@
},
"tsp" : {
},
"Unable to adjust some ingredients!" : {
},
"Unable to complete action." : {
"localizations" : {

View File

@@ -0,0 +1,22 @@
//
// Locale.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 20.03.24.
//
import Foundation
// Ingredient number formatting
func getNumberFormatter() -> NumberFormatter {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.decimalSeparator = UserSettings.shared.decimalNumberSeparator
return formatter
}
let numberFormatter = getNumberFormatter()

View File

@@ -47,14 +47,7 @@ struct RecipeIngredientSection: View {
ServingPickerView(selectedServingSize: $viewModel.observableRecipeDetail.ingredientMultiplier)
}
if viewModel.observableRecipeDetail.ingredientMultiplier != Double(viewModel.observableRecipeDetail.recipeYield) {
HStack() {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
Text("Only highlighted ingredient were adjusted!")
.foregroundStyle(.secondary)
}
}
ForEach(0..<viewModel.observableRecipeDetail.recipeIngredient.count, id: \.self) { ix in
IngredientListItem(
ingredient: $viewModel.observableRecipeDetail.recipeIngredient[ix],
@@ -70,6 +63,16 @@ struct RecipeIngredientSection: View {
}
.padding(4)
}
if viewModel.observableRecipeDetail.ingredientMultiplier != Double(viewModel.observableRecipeDetail.recipeYield) {
HStack() {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
Text("Unable to adjust some ingredients!")
.foregroundStyle(.secondary)
}
}
if viewModel.editMode {
Button {
viewModel.presentIngredientEditView.toggle()
@@ -78,7 +81,9 @@ struct RecipeIngredientSection: View {
}
.buttonStyle(.borderedProminent)
}
}.padding()
}
.padding()
.animation(.easeInOut, value: viewModel.observableRecipeDetail.ingredientMultiplier)
}
}
@@ -94,6 +99,9 @@ fileprivate struct IngredientListItem: View {
@State var modifiedIngredient: AttributedString = ""
@State var isSelected: Bool = false
var unmodified: Bool {
servings == Double(recipeYield) || servings == 0
}
// Drag animation
@State private var dragOffset: CGFloat = 0
@@ -116,7 +124,11 @@ fileprivate struct IngredientListItem: View {
} else {
Image(systemName: "circle")
}
if servings == Double(recipeYield) {
if !unmodified && String(modifiedIngredient.characters) == ingredient {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
}
if unmodified {
Text(ingredient)
.multilineTextAlignment(.leading)
.lineLimit(5)
@@ -175,15 +187,6 @@ fileprivate struct IngredientListItem: View {
struct ServingPickerView: View {
@Binding var selectedServingSize: Double
@State var numberFormatter: NumberFormatter
init(selectedServingSize: Binding<Double>) {
_selectedServingSize = selectedServingSize
numberFormatter = NumberFormatter()
numberFormatter.usesSignificantDigits = true
numberFormatter.maximumFractionDigits = 2
numberFormatter.decimalSeparator = "."
}
// Computed property to handle the text field input and update the selectedServingSize
var body: some View {
@@ -207,7 +210,7 @@ struct ServingPickerView: View {
}
}
.onChange(of: selectedServingSize) { newValue in
if newValue <= 0 { selectedServingSize = 1 }
if newValue < 0 { selectedServingSize = 0 }
else if newValue > 100 { selectedServingSize = 100 }
}
}

View File

@@ -76,6 +76,20 @@ struct SettingsView: View {
}
}
Section {
HStack {
Text("Decimal number format")
Spacer()
Picker("Decimal Separator", selection: $userSettings.decimalNumberSeparator) {
Text("1.42 (Point)").tag(".")
Text("1,42 (Comma)").tag(",")
}
.pickerStyle(.menu)
}
} footer: {
Text("This setting will take effect after the app is restarted. It affects the adjustment of ingredient quantities.")
}
Section {
Toggle(isOn: $userSettings.storeRecipes) {
Text("Offline recipes")
@@ -170,12 +184,6 @@ struct SettingsView: View {
} message: {
Text(viewModel.alertType.getMessage())
}
.onDisappear {
Task {
userSettings.lastUpdate = .distantPast
await appState.updateAllRecipeDetails()
}
}
.task {
await viewModel.getUserData()
}
@@ -239,3 +247,4 @@ extension SettingsView {