Files
PlantGuide/PlantGuide/Data/Mappers/TrefleMapper.swift
treyt 4e3ced4d64 fix: resolve issue #11 - Care Requirements
Automated fix by Tony CI.
Closes #11

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-16 22:42:56 -06:00

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)
)
}