Should work fine
This commit is contained in:
@@ -1,52 +1,146 @@
|
||||
import Foundation
|
||||
import Units
|
||||
|
||||
@dynamicMemberLookup
|
||||
public struct Nutriments: Codable, Sendable {
|
||||
private var storage: [String: Double]
|
||||
// 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 dict = [String: Double]()
|
||||
var v = [String: Double]()
|
||||
var u = [String: String]()
|
||||
|
||||
for key in container.allKeys {
|
||||
if let val = try? container.decode(Double.self, forKey: key) {
|
||||
dict[key.stringValue] = val
|
||||
} else if let valStr = try? container.decode(
|
||||
String.self, forKey: key), let val = Double(valStr)
|
||||
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)
|
||||
{
|
||||
dict[key.stringValue] = val
|
||||
if let doubleFromStr = Double(stringVal) {
|
||||
v[keyStr] = doubleFromStr
|
||||
} else {
|
||||
u[keyStr] = stringVal // It's likely a unit like "kcal", "g", "kj"
|
||||
}
|
||||
}
|
||||
}
|
||||
self.storage = dict
|
||||
self.values = v
|
||||
self.units = u
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: AnyCodingKey.self)
|
||||
for (key, value) in storage {
|
||||
try container.encode(value, forKey: AnyCodingKey(stringValue: key))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
/// Access any nutrient dynamically (e.g. `nutriments.energyKcal`)
|
||||
public subscript(dynamicMember member: String) -> Double? {
|
||||
// Convert camelCase "energyKcal" to snake_case "energy-kcal" or "energy_kcal" logic if needed
|
||||
// For V2, OFF often returns "energy-kcal_100g"
|
||||
let snake = member.camelCaseToSnakeCase()
|
||||
return storage[snake] ?? storage["\(snake)_100g"]
|
||||
?? storage["\(snake)_value"]
|
||||
/// 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)
|
||||
}
|
||||
|
||||
// Specific standard getters
|
||||
public var energyKcal: Double? { self.storage["energy-kcal_100g"] }
|
||||
public var carbohydrates: Double? { self.storage["carbohydrates_100g"] }
|
||||
public var fat: Double? { self.storage["fat_100g"] }
|
||||
public var proteins: Double? { self.storage["proteins_100g"] }
|
||||
public var salt: Double? { self.storage["salt_100g"] }
|
||||
}
|
||||
|
||||
// Helper for String extension used above
|
||||
struct AnyCodingKey: CodingKey {
|
||||
// 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"] }
|
||||
|
||||
// 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<Double>? {
|
||||
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<Double>? {
|
||||
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<Double>? {
|
||||
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<Double>? {
|
||||
guard let rawValue = preparedPerServing,
|
||||
let unitString = preparedUnit ?? unit,
|
||||
let unitEnum = Unit(rawValue: unitString)
|
||||
else { return nil }
|
||||
return UnitValue(value: rawValue, unit: unitEnum)
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user