import Foundation import Units @dynamicMemberLookup public struct Nutriments: Codable, Sendable, Sequence { // We separate values (Doubles) and units (Strings) for efficient, thread-safe storage private let values: [String: Double] private let units: [String: String] public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: AnyCodingKey.self) var v = [String: Double]() var u = [String: String]() for key in container.allKeys { let keyStr = key.stringValue // Try decoding as Double first (most common for _100g, _serving) if let doubleVal = try? container.decode(Double.self, forKey: key) { v[keyStr] = doubleVal } // Try decoding as String (could be a unit OR a number in string format) else if let stringVal = try? container.decode( String.self, forKey: key) { if let doubleFromStr = Double(stringVal) { v[keyStr] = doubleFromStr } else { u[keyStr] = stringVal // It's likely a unit like "kcal", "g", "kj" } } } self.values = v self.units = u } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: AnyCodingKey.self) for (key, val) in values { try container.encode(val, forKey: AnyCodingKey(stringValue: key)) } for (key, unit) in units { try container.encode(unit, forKey: AnyCodingKey(stringValue: key)) } } /// Dynamic lookup: converts `nutriments.energyKcal` -> `Nutrient(name: "energy-kcal")` public subscript(dynamicMember member: String) -> Nutrient { let kebabName = member.camelCaseToKebabCase() return Nutrient(name: kebabName, values: values, units: units) } // MARK: - Sequence Conformance // Allows: nutriments.forEach { nutrient in ... } public func makeIterator() -> AnyIterator { // 1. Collect all keys from values and units let allKeys = Set(values.keys).union(units.keys) // 2. Extract unique base names (e.g., "energy-kcal_100g" -> "energy-kcal") let baseNames = allKeys.compactMap { key -> String? in // Filter out keys that are not nutrient properties guard !key.isEmpty else { return nil } // Remove common suffixes to find the "root" nutrient name let suffixes = [ "_100g", "_serving", "_unit", "_value", "_prepared", "_label", ] var name = key // Sort suffixes by length (descending) to avoid partial matches for suffix in suffixes { if let range = name.range(of: suffix) { name.removeSubrange(range) } } return name } // 3. Create a unique sorted list let uniqueNames = Set(baseNames).sorted() var iterator = uniqueNames.makeIterator() // 4. Return an iterator that produces `Nutrient` views return AnyIterator { guard let name = iterator.next() else { return nil } return Nutrient(name: name, values: self.values, units: self.units) } } } // MARK: - Nutrient View public struct Nutrient: Sendable { public let name: String private let values: [String: Double] private let units: [String: String] internal init( name: String, values: [String: Double], units: [String: String] ) { self.name = name self.values = values self.units = units } // MARK: - Raw Properties (Mapped to V2 JSON keys) public var per100g: Double? { values["\(name)_100g"] } public var perServing: Double? { values["\(name)_serving"] } public var value: Double? { values["\(name)_value"] ?? values[name] } public var unit: String? { units["\(name)_unit"] ?? _unit?.rawValue } // Computed / Legacy public var valueComputed: Double? { values["\(name)_value"] } // Prepared public var preparedPer100g: Double? { values["\(name)_prepared_100g"] } public var preparedPerServing: Double? { values["\(name)_prepared_serving"] } public var preparedValue: Double? { values["\(name)_prepared_value"] ?? values["\(name)_prepared"] } public var preparedUnit: String? { units["\(name)_prepared_unit"] } public var preparedValueComputed: Double? { values["\(name)_prepared_value"] } // MARK: - UnitValue Helpers (Restored) public var per100gUnitValue: UnitValue? { guard let rawValue = per100g ?? value ?? valueComputed, let unitString = unit, let unitEnum = Unit(rawValue: unitString) else { return nil } return UnitValue(value: rawValue, unit: unitEnum) } public var perServingUnitValue: UnitValue? { guard let rawValue = perServing, let unitString = unit, let unitEnum = Unit(rawValue: unitString) else { return nil } return UnitValue(value: rawValue, unit: unitEnum) } public var preparedPer100gUnitValue: UnitValue? { guard let rawValue = preparedPer100g ?? preparedValue ?? preparedValueComputed, let unitString = preparedUnit ?? unit, // Fallback to main unit let unitEnum = Unit(rawValue: unitString) else { return nil } return UnitValue(value: rawValue, unit: unitEnum) } public var preparedPerServingUnitValue: UnitValue? { guard let rawValue = preparedPerServing, let unitString = preparedUnit ?? unit, let unitEnum = Unit(rawValue: unitString) else { return nil } return UnitValue(value: rawValue, unit: unitEnum) } private var _unit: Units.Unit? { switch name { case "energy-kj": .init(rawValue: "kJ") case "energy-kcal": .init(rawValue: "kcal") case "energy": .init(rawValue: "kj") case "energy-from-fat": .init(rawValue: "kJ") case "fat": .init(rawValue: "g") case "saturated-fat": .init(rawValue: "g") case "butyric-acid": .init(rawValue: "g") case "caproic-acid": .init(rawValue: "g") case "caprylic-acid": .init(rawValue: "g") case "capric-acid": .init(rawValue: "g") case "lauric-acid": .init(rawValue: "g") case "myristic-acid": .init(rawValue: "g") case "palmitic-acid": .init(rawValue: "g") case "Psicose": .init(rawValue: "g") case "stearic-acid": .init(rawValue: "g") case "arachidic-acid": .init(rawValue: "g") case "behenic-acid": .init(rawValue: "g") case "lignoceric-acid": .init(rawValue: "g") case "cerotic-acid": .init(rawValue: "g") case "montanic-acid": .init(rawValue: "g") case "melissic-acid": .init(rawValue: "g") case "unsaturated-fat": .init(rawValue: "g") case "monounsaturated-fat": .init(rawValue: "g") case "polyunsaturated-fat": .init(rawValue: "g") case "omega-3-fat": .init(rawValue: "mg") case "alpha-linolenic-acid": .init(rawValue: "g") case "eicosapentaenoic-acid": .init(rawValue: "g") case "docosahexaenoic-acid": .init(rawValue: "g") case "omega-6-fat": .init(rawValue: "mg") case "linoleic-acid": .init(rawValue: "g") case "arachidonic-acid": .init(rawValue: "g") case "gamma-linolenic-acid": .init(rawValue: "g") case "dihomo-gamma-linolenic-acid": .init(rawValue: "g") case "omega-9-fat": .init(rawValue: "mg") case "oleic-acid": .init(rawValue: "g") case "elaidic-acid": .init(rawValue: "g") case "gondoic-acid": .init(rawValue: "g") case "mead-acid": .init(rawValue: "g") case "erucic-acid": .init(rawValue: "g") case "nervonic-acid": .init(rawValue: "g") case "trans-fat": .init(rawValue: "g") case "cholesterol": .init(rawValue: "mg") case "gamma-oryzanol": .init(rawValue: "mg") case "carbohydrates-total": .init(rawValue: "g") case "carbohydrates": .init(rawValue: "g") case "sugars": .init(rawValue: "g") case "added-sugars": .init(rawValue: "g") case "sucrose": .init(rawValue: "g") case "glucose": .init(rawValue: "g") case "fructose": .init(rawValue: "g") case "oligosaccharide": .init(rawValue: "g") case "lactose": .init(rawValue: "g") case "galactose": .init(rawValue: "g") case "maltose": .init(rawValue: "g") case "maltodextrins": .init(rawValue: "g") case "starch": .init(rawValue: "g") case "polyols": .init(rawValue: "g") case "Erythritol": .init(rawValue: "g") case "Isomalt": .init(rawValue: "g") case "Maltitol": .init(rawValue: "g") case "Sorbitol": .init(rawValue: "g") case "fiber": .init(rawValue: "g") case "soluble-fiber": .init(rawValue: "g") case "insoluble-fiber": .init(rawValue: "g") case "proteins": .init(rawValue: "g") case "casein": .init(rawValue: "g") case "serum-proteins": .init(rawValue: "g") case "nucleotides": .init(rawValue: "g") case "salt": .init(rawValue: "g") case "added-salt": .init(rawValue: "g") case "sodium": .init(rawValue: "g") case "alcohol": .init(rawValue: "% vol") case "vitamin-a": .init(rawValue: "µg") case "beta-carotene": .init(rawValue: "g") case "vitamin-d": .init(rawValue: "µg") case "vitamin-e": .init(rawValue: "mg") case "vitamin-k": .init(rawValue: "µg") case "vitamin-c": .init(rawValue: "mg") case "vitamin-b1": .init(rawValue: "mg") case "vitamin-b2": .init(rawValue: "mg") case "vitamin-pp": .init(rawValue: "mg") case "vitamin-b6": .init(rawValue: "mg") case "vitamin-b9": .init(rawValue: "µg") case "folates": .init(rawValue: "µg") case "vitamin-b12": .init(rawValue: "µg") case "biotin": .init(rawValue: "µg") case "pantothenic-acid": .init(rawValue: "mg") case "silica": .init(rawValue: "mg") case "bicarbonate": .init(rawValue: "mg") case "Sulphate": .init(rawValue: "mg") case "Nitrate": .init(rawValue: "mg") case "potassium": .init(rawValue: "mg") case "chloride": .init(rawValue: "mg") case "calcium": .init(rawValue: "mg") case "phosphorus": .init(rawValue: "mg") case "iron": .init(rawValue: "mg") case "magnesium": .init(rawValue: "mg") case "zinc": .init(rawValue: "mg") case "copper": .init(rawValue: "mg") case "manganese": .init(rawValue: "mg") case "fluoride": .init(rawValue: "mg") case "selenium": .init(rawValue: "µg") case "chromium": .init(rawValue: "µg") case "molybdenum": .init(rawValue: "µg") case "iodine": .init(rawValue: "µg") case "caffeine": .init(rawValue: "mg") case "taurine": .init(rawValue: "g") case "chlorophyl": .init(rawValue: "g") case "choline": .init(rawValue: "g") case "phylloquinone": .init(rawValue: "g") case "beta-glucan": .init(rawValue: "g") case "inositol": .init(rawValue: "g") case "carnitine": .init(rawValue: "g") case "melatonin": .init(rawValue: "µg") case "methylsulfonylmethane": .init(rawValue: "mg") case "creatine": .init(rawValue: "g") case "l-citrulline": .init(rawValue: "mg") case "l-glutamine": .init(rawValue: "mg") case "bcaa": .init(rawValue: "g") case "l-valine": .init(rawValue: "mg") case "l-leucine": .init(rawValue: "mg") case "l-isoleucine": .init(rawValue: "mg") case "l-arginine": .init(rawValue: "mg") case "l-cysteine": .init(rawValue: "mg") case "l-Glutathione": .init(rawValue: "mg") case "iron II sulphate monohydrate": .init(rawValue: "mg") case "potassium iodide": .init(rawValue: "mg") case "copper II sulphate pentahydrate": .init(rawValue: "mg") case "manganous sulphate monohydrate": .init(rawValue: "mg") case "zinc sulphate monohydrate": .init(rawValue: "mg") case "sodium selenite": .init(rawValue: "mg") case "calcium iodate anhydrous": .init(rawValue: "mg") case "cassia gum": .init(rawValue: "mg") case "ammonium chloride": .init(rawValue: "mg") case "choline chloride": .init(rawValue: "mg") default: nil } } } // MARK: - Helpers private struct AnyCodingKey: CodingKey { var stringValue: String var intValue: Int? init(stringValue: String) { self.stringValue = stringValue } init?(intValue: Int) { return nil } } extension String { // Converts "energyKcal" -> "energy-kcal" fileprivate func camelCaseToKebabCase() -> String { let pattern = "([a-z0-9])([A-Z])" let regex = try! NSRegularExpression(pattern: pattern, options: []) let range = NSRange(location: 0, length: self.count) return regex.stringByReplacingMatches( in: self, options: [], range: range, withTemplate: "$1-$2" ).lowercased() } }