500 lines
20 KiB
Swift
500 lines
20 KiB
Swift
//
|
|
// TrefleMapper.swift
|
|
// PlantGuide
|
|
//
|
|
// Created on 2026-01-21.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
// MARK: - TrefleMapper
|
|
|
|
/// Maps Trefle API DTOs to domain entities.
|
|
///
|
|
/// This mapper provides conversion functions for transforming Trefle API
|
|
/// responses into PlantCareInfo domain entities and related care requirement types.
|
|
/// All mapping functions handle nil/missing data gracefully with sensible defaults.
|
|
struct TrefleMapper {
|
|
|
|
// MARK: - Primary Mapping
|
|
|
|
/// Maps a Trefle species DTO to a PlantCareInfo domain entity.
|
|
///
|
|
/// This function extracts all available care information from the Trefle species data,
|
|
/// including growth requirements, environmental preferences, and blooming seasons.
|
|
///
|
|
/// - Parameter species: The detailed species information from the Trefle API.
|
|
/// - Returns: A `PlantCareInfo` domain entity populated with care requirements.
|
|
static func mapToPlantCareInfo(from species: TrefleSpeciesDTO) -> PlantCareInfo {
|
|
let growth = species.growth
|
|
let specifications = species.specifications
|
|
let family = species.family?.lowercased()
|
|
|
|
// Use family-aware defaults when specific growth data is missing.
|
|
// Different plant families have very different care needs, so a
|
|
// cactus shouldn't get the same defaults as a tropical aroid.
|
|
let familyDefaults = familyCareDefaults(for: family)
|
|
|
|
return PlantCareInfo(
|
|
id: UUID(),
|
|
scientificName: species.scientificName,
|
|
commonName: species.commonName,
|
|
lightRequirement: mapToLightRequirement(from: growth?.light, familyDefault: familyDefaults.light),
|
|
wateringSchedule: mapToWateringSchedule(from: growth, familyDefault: familyDefaults.watering),
|
|
temperatureRange: mapToTemperatureRange(from: growth, familyDefault: familyDefaults.temperature),
|
|
fertilizerSchedule: mapToFertilizerSchedule(from: growth),
|
|
humidity: mapToHumidityLevel(from: growth?.atmosphericHumidity),
|
|
growthRate: mapToGrowthRate(from: specifications?.growthRate),
|
|
bloomingSeason: mapToBloomingSeason(from: growth?.bloomMonths),
|
|
additionalNotes: buildAdditionalNotes(from: species),
|
|
sourceURL: URL(string: "https://trefle.io/api/v1/species/\(species.id)"),
|
|
trefleID: species.id
|
|
)
|
|
}
|
|
|
|
// MARK: - Light Requirement Mapping
|
|
|
|
/// Maps a Trefle light scale value to a LightRequirement enum.
|
|
///
|
|
/// Trefle uses a 0-10 scale where 0 is full shade and 10 is full sun.
|
|
/// This function maps that scale to the app's LightRequirement categories.
|
|
///
|
|
/// - Parameter light: The Trefle light value (0-10 scale), or nil if not available.
|
|
/// - Returns: The corresponding `LightRequirement` enum value.
|
|
/// Defaults to `.partialShade` if the input is nil.
|
|
///
|
|
/// Mapping:
|
|
/// - 0-2: `.fullShade` - Very low light conditions
|
|
/// - 3-4: `.lowLight` - Low light but not complete shade
|
|
/// - 5-6: `.partialShade` - Moderate, indirect light
|
|
/// - 7-10: `.fullSun` - Direct sunlight for most of the day
|
|
static func mapToLightRequirement(from light: Int?, familyDefault: LightRequirement = .partialShade) -> LightRequirement {
|
|
guard let light = light else {
|
|
return familyDefault
|
|
}
|
|
|
|
switch light {
|
|
case 0...2:
|
|
return .fullShade
|
|
case 3...4:
|
|
return .lowLight
|
|
case 5...6:
|
|
return .partialShade
|
|
case 7...10:
|
|
return .fullSun
|
|
default:
|
|
return .partialShade
|
|
}
|
|
}
|
|
|
|
// MARK: - Watering Schedule Mapping
|
|
|
|
/// Maps Trefle growth data to a WateringSchedule.
|
|
///
|
|
/// This function determines watering frequency and amount based on the plant's
|
|
/// atmospheric and soil humidity requirements. Plants with higher humidity needs
|
|
/// typically require more frequent but lighter watering, while those with lower
|
|
/// needs benefit from less frequent but thorough watering.
|
|
///
|
|
/// - Parameter growth: The Trefle growth data containing humidity requirements.
|
|
/// - Returns: A `WateringSchedule` with appropriate frequency and amount.
|
|
/// Defaults to weekly/moderate if growth data is nil.
|
|
///
|
|
/// Mapping based on humidity levels (0-10 scale):
|
|
/// - High humidity (7-10): Weekly frequency with light watering
|
|
/// - Medium humidity (4-6): Twice weekly with moderate watering
|
|
/// - Low humidity (0-3): Weekly with thorough watering
|
|
static func mapToWateringSchedule(
|
|
from growth: TrefleGrowthDTO?,
|
|
familyDefault: WateringSchedule = WateringSchedule(frequency: .weekly, amount: .moderate)
|
|
) -> WateringSchedule {
|
|
guard let growth = growth else {
|
|
return familyDefault
|
|
}
|
|
|
|
// Use atmospheric humidity as primary indicator, fall back to soil humidity
|
|
let humidityLevel = growth.atmosphericHumidity ?? growth.soilHumidity
|
|
|
|
guard let humidity = humidityLevel else {
|
|
return familyDefault
|
|
}
|
|
|
|
switch humidity {
|
|
case 7...10:
|
|
// High humidity plants need frequent, light watering
|
|
return WateringSchedule(frequency: .weekly, amount: .light)
|
|
case 4...6:
|
|
// Medium humidity plants need regular, moderate watering
|
|
return WateringSchedule(frequency: .twiceWeekly, amount: .moderate)
|
|
case 0...3:
|
|
// Low humidity plants (often drought-tolerant) need less frequent but thorough watering
|
|
return WateringSchedule(frequency: .weekly, amount: .thorough)
|
|
default:
|
|
return WateringSchedule(frequency: .weekly, amount: .moderate)
|
|
}
|
|
}
|
|
|
|
// MARK: - Temperature Range Mapping
|
|
|
|
/// Maps Trefle growth data to a TemperatureRange.
|
|
///
|
|
/// This function extracts minimum and maximum temperature tolerances from the
|
|
/// Trefle growth data and determines frost tolerance based on whether the plant
|
|
/// can survive temperatures below 0 degrees Celsius.
|
|
///
|
|
/// - Parameter growth: The Trefle growth data containing temperature information.
|
|
/// - Returns: A `TemperatureRange` with min/max values and frost tolerance.
|
|
/// Defaults to 15-30 degrees Celsius if growth data is nil.
|
|
static func mapToTemperatureRange(
|
|
from growth: TrefleGrowthDTO?,
|
|
familyDefault: TemperatureRange = TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 30.0, optimalCelsius: nil, frostTolerant: false)
|
|
) -> TemperatureRange {
|
|
guard let growth = growth else {
|
|
return familyDefault
|
|
}
|
|
|
|
let minTemp = growth.minimumTemperature?.degC ?? 15.0
|
|
let maxTemp = growth.maximumTemperature?.degC ?? 30.0
|
|
|
|
// Calculate optimal as midpoint between min and max if both are available
|
|
let optimalTemp: Double?
|
|
if growth.minimumTemperature?.degC != nil && growth.maximumTemperature?.degC != nil {
|
|
optimalTemp = (minTemp + maxTemp) / 2.0
|
|
} else {
|
|
optimalTemp = nil
|
|
}
|
|
|
|
// Plant is frost tolerant if it can survive temperatures below 0 degrees Celsius
|
|
let frostTolerant = minTemp < 0.0
|
|
|
|
return TemperatureRange(
|
|
minimumCelsius: minTemp,
|
|
maximumCelsius: maxTemp,
|
|
optimalCelsius: optimalTemp,
|
|
frostTolerant: frostTolerant
|
|
)
|
|
}
|
|
|
|
// MARK: - Fertilizer Schedule Mapping
|
|
|
|
/// Maps Trefle growth data to a FertilizerSchedule.
|
|
///
|
|
/// This function determines fertilizer frequency and type based on the plant's
|
|
/// soil nutrient requirements. Plants with higher nutrient needs require more
|
|
/// frequent fertilization, while those with lower needs can use organic fertilizers
|
|
/// applied less frequently.
|
|
///
|
|
/// - Parameter growth: The Trefle growth data containing soil nutrient requirements.
|
|
/// - Returns: A `FertilizerSchedule` with appropriate frequency and type,
|
|
/// or nil if soil nutrient data is not available.
|
|
///
|
|
/// Mapping based on soil nutriment levels (0-10 scale):
|
|
/// - High needs (7-10): Biweekly with balanced fertilizer
|
|
/// - Medium needs (4-6): Monthly with balanced fertilizer
|
|
/// - Low needs (0-3): Quarterly with organic fertilizer
|
|
static func mapToFertilizerSchedule(from growth: TrefleGrowthDTO?) -> FertilizerSchedule? {
|
|
guard let soilNutriments = growth?.soilNutriments else {
|
|
return nil
|
|
}
|
|
|
|
switch soilNutriments {
|
|
case 7...10:
|
|
// High nutrient needs - frequent balanced fertilization
|
|
return FertilizerSchedule(frequency: .biweekly, type: .balanced)
|
|
case 4...6:
|
|
// Medium nutrient needs - monthly balanced fertilization
|
|
return FertilizerSchedule(frequency: .monthly, type: .balanced)
|
|
case 0...3:
|
|
// Low nutrient needs - occasional organic fertilization
|
|
return FertilizerSchedule(frequency: .quarterly, type: .organic)
|
|
default:
|
|
return FertilizerSchedule(frequency: .monthly, type: .balanced)
|
|
}
|
|
}
|
|
|
|
// MARK: - Humidity Level Mapping
|
|
|
|
/// Maps a Trefle atmospheric humidity value to a HumidityLevel enum.
|
|
///
|
|
/// Trefle uses a 0-10 scale for atmospheric humidity requirements.
|
|
/// This function maps that scale to the app's HumidityLevel categories.
|
|
///
|
|
/// - Parameter humidity: The Trefle atmospheric humidity value (0-10 scale),
|
|
/// or nil if not available.
|
|
/// - Returns: The corresponding `HumidityLevel` enum value, or nil if input is nil.
|
|
///
|
|
/// Mapping:
|
|
/// - 0-2: `.low` - Below 30% humidity
|
|
/// - 3-5: `.moderate` - 30-50% humidity
|
|
/// - 6-8: `.high` - 50-70% humidity
|
|
/// - 9-10: `.veryHigh` - Above 70% humidity
|
|
static func mapToHumidityLevel(from humidity: Int?) -> HumidityLevel? {
|
|
guard let humidity = humidity else {
|
|
return nil
|
|
}
|
|
|
|
switch humidity {
|
|
case 0...2:
|
|
return .low
|
|
case 3...5:
|
|
return .moderate
|
|
case 6...8:
|
|
return .high
|
|
case 9...10:
|
|
return .veryHigh
|
|
default:
|
|
return .moderate
|
|
}
|
|
}
|
|
|
|
// MARK: - Growth Rate Mapping
|
|
|
|
/// Maps a Trefle growth rate string to a GrowthRate enum.
|
|
///
|
|
/// Trefle provides growth rate as a string (e.g., "slow", "moderate", "rapid").
|
|
/// This function maps those values to the app's GrowthRate enum.
|
|
///
|
|
/// - Parameter growthRate: The Trefle growth rate string, or nil if not available.
|
|
/// - Returns: The corresponding `GrowthRate` enum value, or nil if input is nil
|
|
/// or doesn't match a known value.
|
|
static func mapToGrowthRate(from growthRate: String?) -> GrowthRate? {
|
|
guard let growthRate = growthRate?.lowercased() else {
|
|
return nil
|
|
}
|
|
|
|
switch growthRate {
|
|
case "slow":
|
|
return .slow
|
|
case "moderate", "medium":
|
|
return .moderate
|
|
case "rapid", "fast":
|
|
return .fast
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Blooming Season Mapping
|
|
|
|
/// Maps Trefle bloom months to an array of Season values.
|
|
///
|
|
/// Trefle provides bloom months as an array of three-letter month abbreviations
|
|
/// (e.g., ["mar", "apr", "may"]). This function converts those months to the
|
|
/// corresponding seasons based on Northern Hemisphere conventions.
|
|
///
|
|
/// - Parameter bloomMonths: An array of month abbreviations, or nil if not available.
|
|
/// - Returns: An array of unique `Season` values when the plant blooms,
|
|
/// or nil if bloom month data is not available.
|
|
///
|
|
/// Month to Season Mapping (Northern Hemisphere):
|
|
/// - December, January, February: `.winter`
|
|
/// - March, April, May: `.spring`
|
|
/// - June, July, August: `.summer`
|
|
/// - September, October, November: `.fall`
|
|
static func mapToBloomingSeason(from bloomMonths: [String]?) -> [Season]? {
|
|
guard let bloomMonths = bloomMonths, !bloomMonths.isEmpty else {
|
|
return nil
|
|
}
|
|
|
|
var seasons = Set<Season>()
|
|
|
|
for month in bloomMonths {
|
|
if let season = monthToSeason(month.lowercased()) {
|
|
seasons.insert(season)
|
|
}
|
|
}
|
|
|
|
guard !seasons.isEmpty else {
|
|
return nil
|
|
}
|
|
|
|
// Return seasons in natural order: spring, summer, fall, winter
|
|
let orderedSeasons: [Season] = [.spring, .summer, .fall, .winter]
|
|
return orderedSeasons.filter { seasons.contains($0) }
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
|
|
/// Converts a month abbreviation to its corresponding season.
|
|
///
|
|
/// - Parameter month: A three-letter month abbreviation (lowercase).
|
|
/// - Returns: The corresponding `Season`, or nil if the abbreviation is not recognized.
|
|
private static func monthToSeason(_ month: String) -> Season? {
|
|
switch month {
|
|
case "dec", "jan", "feb":
|
|
return .winter
|
|
case "mar", "apr", "may":
|
|
return .spring
|
|
case "jun", "jul", "aug":
|
|
return .summer
|
|
case "sep", "oct", "nov":
|
|
return .fall
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Builds additional care notes from species data.
|
|
///
|
|
/// This function compiles relevant information that doesn't fit into other
|
|
/// structured fields, such as pH requirements and toxicity warnings.
|
|
///
|
|
/// - Parameter species: The Trefle species DTO.
|
|
/// - Returns: A string containing additional care notes, or nil if no relevant data.
|
|
private static func buildAdditionalNotes(from species: TrefleSpeciesDTO) -> String? {
|
|
var notes: [String] = []
|
|
|
|
// Add pH range information if available
|
|
if let phMin = species.growth?.phMinimum, let phMax = species.growth?.phMaximum {
|
|
notes.append("Soil pH: \(String(format: "%.1f", phMin)) - \(String(format: "%.1f", phMax))")
|
|
} else if let phMin = species.growth?.phMinimum {
|
|
notes.append("Minimum soil pH: \(String(format: "%.1f", phMin))")
|
|
} else if let phMax = species.growth?.phMaximum {
|
|
notes.append("Maximum soil pH: \(String(format: "%.1f", phMax))")
|
|
}
|
|
|
|
// Add toxicity warning if available
|
|
if let toxicity = species.specifications?.toxicity?.lowercased(), toxicity != "none" {
|
|
notes.append("Toxicity: \(toxicity.capitalized)")
|
|
}
|
|
|
|
// Add family information for reference
|
|
if let family = species.family {
|
|
notes.append("Family: \(family)")
|
|
}
|
|
|
|
return notes.isEmpty ? nil : notes.joined(separator: ". ")
|
|
}
|
|
|
|
// MARK: - Family-Aware Defaults
|
|
|
|
/// Care defaults grouped by plant family
|
|
struct FamilyCareDefaults {
|
|
let light: LightRequirement
|
|
let watering: WateringSchedule
|
|
let temperature: TemperatureRange
|
|
}
|
|
|
|
/// Returns care defaults appropriate for the given plant family.
|
|
///
|
|
/// When the Trefle API returns a species without detailed growth data,
|
|
/// these family-based defaults provide more accurate care recommendations
|
|
/// than a single generic default for all plants.
|
|
///
|
|
/// - Parameter family: The lowercased plant family name (e.g., "cactaceae").
|
|
/// - Returns: Care defaults tuned for that family.
|
|
static func familyCareDefaults(for family: String?) -> FamilyCareDefaults {
|
|
guard let family = family else {
|
|
return genericDefaults
|
|
}
|
|
|
|
switch family {
|
|
// Cacti and succulents
|
|
case "cactaceae":
|
|
return FamilyCareDefaults(
|
|
light: .fullSun,
|
|
watering: WateringSchedule(frequency: .biweekly, amount: .light),
|
|
temperature: TemperatureRange(minimumCelsius: 10.0, maximumCelsius: 35.0, frostTolerant: false)
|
|
)
|
|
|
|
// Succulents (stonecrop family - Jade, Echeveria, Sedum)
|
|
case "crassulaceae":
|
|
return FamilyCareDefaults(
|
|
light: .fullSun,
|
|
watering: WateringSchedule(frequency: .biweekly, amount: .moderate),
|
|
temperature: TemperatureRange(minimumCelsius: 10.0, maximumCelsius: 30.0, frostTolerant: false)
|
|
)
|
|
|
|
// Aloe, Haworthia
|
|
case "asphodelaceae":
|
|
return FamilyCareDefaults(
|
|
light: .partialShade,
|
|
watering: WateringSchedule(frequency: .biweekly, amount: .moderate),
|
|
temperature: TemperatureRange(minimumCelsius: 10.0, maximumCelsius: 30.0, frostTolerant: false)
|
|
)
|
|
|
|
// Tropical aroids (Monstera, Pothos, Philodendron)
|
|
case "araceae":
|
|
return FamilyCareDefaults(
|
|
light: .partialShade,
|
|
watering: WateringSchedule(frequency: .weekly, amount: .moderate),
|
|
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 30.0, frostTolerant: false)
|
|
)
|
|
|
|
// Ferns
|
|
case "polypodiaceae", "pteridaceae", "dryopteridaceae", "aspleniaceae":
|
|
return FamilyCareDefaults(
|
|
light: .lowLight,
|
|
watering: WateringSchedule(frequency: .twiceWeekly, amount: .moderate),
|
|
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 26.0, frostTolerant: false)
|
|
)
|
|
|
|
// Orchids
|
|
case "orchidaceae":
|
|
return FamilyCareDefaults(
|
|
light: .partialShade,
|
|
watering: WateringSchedule(frequency: .weekly, amount: .light),
|
|
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 28.0, frostTolerant: false)
|
|
)
|
|
|
|
// Ficus, rubber plant family
|
|
case "moraceae":
|
|
return FamilyCareDefaults(
|
|
light: .partialShade,
|
|
watering: WateringSchedule(frequency: .weekly, amount: .moderate),
|
|
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 30.0, frostTolerant: false)
|
|
)
|
|
|
|
// Spider plant, snake plant, asparagus fern family
|
|
case "asparagaceae":
|
|
return FamilyCareDefaults(
|
|
light: .lowLight,
|
|
watering: WateringSchedule(frequency: .weekly, amount: .moderate),
|
|
temperature: TemperatureRange(minimumCelsius: 12.0, maximumCelsius: 30.0, frostTolerant: false)
|
|
)
|
|
|
|
// Rose family
|
|
case "rosaceae":
|
|
return FamilyCareDefaults(
|
|
light: .fullSun,
|
|
watering: WateringSchedule(frequency: .twiceWeekly, amount: .thorough),
|
|
temperature: TemperatureRange(minimumCelsius: -5.0, maximumCelsius: 32.0, frostTolerant: true)
|
|
)
|
|
|
|
// Palms
|
|
case "arecaceae":
|
|
return FamilyCareDefaults(
|
|
light: .partialShade,
|
|
watering: WateringSchedule(frequency: .weekly, amount: .moderate),
|
|
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 32.0, frostTolerant: false)
|
|
)
|
|
|
|
// Begonias
|
|
case "begoniaceae":
|
|
return FamilyCareDefaults(
|
|
light: .partialShade,
|
|
watering: WateringSchedule(frequency: .twiceWeekly, amount: .light),
|
|
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 28.0, frostTolerant: false)
|
|
)
|
|
|
|
// Marantaceae (Calathea, Prayer plant)
|
|
case "marantaceae":
|
|
return FamilyCareDefaults(
|
|
light: .lowLight,
|
|
watering: WateringSchedule(frequency: .twiceWeekly, amount: .moderate),
|
|
temperature: TemperatureRange(minimumCelsius: 16.0, maximumCelsius: 27.0, frostTolerant: false)
|
|
)
|
|
|
|
default:
|
|
return genericDefaults
|
|
}
|
|
}
|
|
|
|
/// Generic fallback defaults when no family information is available
|
|
private static let genericDefaults = FamilyCareDefaults(
|
|
light: .partialShade,
|
|
watering: WateringSchedule(frequency: .weekly, amount: .moderate),
|
|
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 30.0, frostTolerant: false)
|
|
)
|
|
}
|