Merge pull request #16 from akatreyt/fix/issue-11
fix: resolve #11 - Care Requirements
This commit is contained in:
@@ -28,14 +28,20 @@ struct TrefleMapper {
|
|||||||
static func mapToPlantCareInfo(from species: TrefleSpeciesDTO) -> PlantCareInfo {
|
static func mapToPlantCareInfo(from species: TrefleSpeciesDTO) -> PlantCareInfo {
|
||||||
let growth = species.growth
|
let growth = species.growth
|
||||||
let specifications = species.specifications
|
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(
|
return PlantCareInfo(
|
||||||
id: UUID(),
|
id: UUID(),
|
||||||
scientificName: species.scientificName,
|
scientificName: species.scientificName,
|
||||||
commonName: species.commonName,
|
commonName: species.commonName,
|
||||||
lightRequirement: mapToLightRequirement(from: growth?.light),
|
lightRequirement: mapToLightRequirement(from: growth?.light, familyDefault: familyDefaults.light),
|
||||||
wateringSchedule: mapToWateringSchedule(from: growth),
|
wateringSchedule: mapToWateringSchedule(from: growth, familyDefault: familyDefaults.watering),
|
||||||
temperatureRange: mapToTemperatureRange(from: growth),
|
temperatureRange: mapToTemperatureRange(from: growth, familyDefault: familyDefaults.temperature),
|
||||||
fertilizerSchedule: mapToFertilizerSchedule(from: growth),
|
fertilizerSchedule: mapToFertilizerSchedule(from: growth),
|
||||||
humidity: mapToHumidityLevel(from: growth?.atmosphericHumidity),
|
humidity: mapToHumidityLevel(from: growth?.atmosphericHumidity),
|
||||||
growthRate: mapToGrowthRate(from: specifications?.growthRate),
|
growthRate: mapToGrowthRate(from: specifications?.growthRate),
|
||||||
@@ -62,9 +68,9 @@ struct TrefleMapper {
|
|||||||
/// - 3-4: `.lowLight` - Low light but not complete shade
|
/// - 3-4: `.lowLight` - Low light but not complete shade
|
||||||
/// - 5-6: `.partialShade` - Moderate, indirect light
|
/// - 5-6: `.partialShade` - Moderate, indirect light
|
||||||
/// - 7-10: `.fullSun` - Direct sunlight for most of the day
|
/// - 7-10: `.fullSun` - Direct sunlight for most of the day
|
||||||
static func mapToLightRequirement(from light: Int?) -> LightRequirement {
|
static func mapToLightRequirement(from light: Int?, familyDefault: LightRequirement = .partialShade) -> LightRequirement {
|
||||||
guard let light = light else {
|
guard let light = light else {
|
||||||
return .partialShade
|
return familyDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
switch light {
|
switch light {
|
||||||
@@ -98,16 +104,19 @@ struct TrefleMapper {
|
|||||||
/// - High humidity (7-10): Weekly frequency with light watering
|
/// - High humidity (7-10): Weekly frequency with light watering
|
||||||
/// - Medium humidity (4-6): Twice weekly with moderate watering
|
/// - Medium humidity (4-6): Twice weekly with moderate watering
|
||||||
/// - Low humidity (0-3): Weekly with thorough watering
|
/// - Low humidity (0-3): Weekly with thorough watering
|
||||||
static func mapToWateringSchedule(from growth: TrefleGrowthDTO?) -> WateringSchedule {
|
static func mapToWateringSchedule(
|
||||||
|
from growth: TrefleGrowthDTO?,
|
||||||
|
familyDefault: WateringSchedule = WateringSchedule(frequency: .weekly, amount: .moderate)
|
||||||
|
) -> WateringSchedule {
|
||||||
guard let growth = growth else {
|
guard let growth = growth else {
|
||||||
return WateringSchedule(frequency: .weekly, amount: .moderate)
|
return familyDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use atmospheric humidity as primary indicator, fall back to soil humidity
|
// Use atmospheric humidity as primary indicator, fall back to soil humidity
|
||||||
let humidityLevel = growth.atmosphericHumidity ?? growth.soilHumidity
|
let humidityLevel = growth.atmosphericHumidity ?? growth.soilHumidity
|
||||||
|
|
||||||
guard let humidity = humidityLevel else {
|
guard let humidity = humidityLevel else {
|
||||||
return WateringSchedule(frequency: .weekly, amount: .moderate)
|
return familyDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
switch humidity {
|
switch humidity {
|
||||||
@@ -136,14 +145,12 @@ struct TrefleMapper {
|
|||||||
/// - Parameter growth: The Trefle growth data containing temperature information.
|
/// - Parameter growth: The Trefle growth data containing temperature information.
|
||||||
/// - Returns: A `TemperatureRange` with min/max values and frost tolerance.
|
/// - Returns: A `TemperatureRange` with min/max values and frost tolerance.
|
||||||
/// Defaults to 15-30 degrees Celsius if growth data is nil.
|
/// Defaults to 15-30 degrees Celsius if growth data is nil.
|
||||||
static func mapToTemperatureRange(from growth: TrefleGrowthDTO?) -> TemperatureRange {
|
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 {
|
guard let growth = growth else {
|
||||||
return TemperatureRange(
|
return familyDefault
|
||||||
minimumCelsius: 15.0,
|
|
||||||
maximumCelsius: 30.0,
|
|
||||||
optimalCelsius: nil,
|
|
||||||
frostTolerant: false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let minTemp = growth.minimumTemperature?.degC ?? 15.0
|
let minTemp = growth.minimumTemperature?.degC ?? 15.0
|
||||||
@@ -358,4 +365,135 @@ struct TrefleMapper {
|
|||||||
|
|
||||||
return notes.isEmpty ? nil : notes.joined(separator: ". ")
|
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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,13 +228,18 @@ final class FetchPlantCareUseCase: FetchPlantCareUseCaseProtocol, @unchecked Sen
|
|||||||
// Search for the plant by scientific name
|
// Search for the plant by scientific name
|
||||||
let searchResponse = try await trefleAPIService.searchPlants(query: scientificName, page: 1)
|
let searchResponse = try await trefleAPIService.searchPlants(query: scientificName, page: 1)
|
||||||
|
|
||||||
// Take the first result from the search
|
guard !searchResponse.data.isEmpty else {
|
||||||
guard let firstResult = searchResponse.data.first else {
|
|
||||||
throw FetchPlantCareError.speciesNotFound(name: scientificName)
|
throw FetchPlantCareError.speciesNotFound(name: scientificName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find the best matching result by comparing scientific names.
|
||||||
|
// The Trefle search is fuzzy, so the first result may not be the
|
||||||
|
// correct species. We prefer an exact genus+species match, then
|
||||||
|
// a genus-only match, and finally fall back to the first result.
|
||||||
|
let bestMatch = findBestMatch(for: scientificName, in: searchResponse.data)
|
||||||
|
|
||||||
// Fetch full species details using the slug
|
// Fetch full species details using the slug
|
||||||
let speciesResponse = try await trefleAPIService.getSpecies(slug: firstResult.slug)
|
let speciesResponse = try await trefleAPIService.getSpecies(slug: bestMatch.slug)
|
||||||
let species = speciesResponse.data
|
let species = speciesResponse.data
|
||||||
|
|
||||||
// Map to PlantCareInfo using TrefleMapper
|
// Map to PlantCareInfo using TrefleMapper
|
||||||
@@ -257,6 +262,46 @@ final class FetchPlantCareUseCase: FetchPlantCareUseCaseProtocol, @unchecked Sen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finds the best matching search result for the given scientific name.
|
||||||
|
///
|
||||||
|
/// Matching priority:
|
||||||
|
/// 1. Exact scientific name match (case-insensitive)
|
||||||
|
/// 2. Scientific name starts with the query (handles author suffixes like "L.")
|
||||||
|
/// 3. Same genus (first word of scientific name)
|
||||||
|
/// 4. First result as fallback
|
||||||
|
private func findBestMatch(
|
||||||
|
for scientificName: String,
|
||||||
|
in results: [TreflePlantSummaryDTO]
|
||||||
|
) -> TreflePlantSummaryDTO {
|
||||||
|
let queryLower = scientificName.lowercased().trimmingCharacters(in: .whitespaces)
|
||||||
|
let queryGenus = queryLower.components(separatedBy: " ").first ?? queryLower
|
||||||
|
|
||||||
|
// 1. Exact match
|
||||||
|
if let exact = results.first(where: {
|
||||||
|
$0.scientificName.lowercased() == queryLower
|
||||||
|
}) {
|
||||||
|
return exact
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Result whose scientific name starts with our query
|
||||||
|
// (handles cases like "Rosa gallica L." matching "Rosa gallica")
|
||||||
|
if let prefixMatch = results.first(where: {
|
||||||
|
$0.scientificName.lowercased().hasPrefix(queryLower)
|
||||||
|
}) {
|
||||||
|
return prefixMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Same genus match
|
||||||
|
if let genusMatch = results.first(where: {
|
||||||
|
$0.scientificName.lowercased().components(separatedBy: " ").first == queryGenus
|
||||||
|
}) {
|
||||||
|
return genusMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fall back to first result
|
||||||
|
return results.first!
|
||||||
|
}
|
||||||
|
|
||||||
func execute(trefleId: Int, forceRefresh: Bool = false) async throws -> PlantCareInfo {
|
func execute(trefleId: Int, forceRefresh: Bool = false) async throws -> PlantCareInfo {
|
||||||
// 1. Check cache first (unless force refresh is requested)
|
// 1. Check cache first (unless force refresh is requested)
|
||||||
if !forceRefresh, let cached = try? await cacheRepository?.fetch(trefleID: trefleId) {
|
if !forceRefresh, let cached = try? await cacheRepository?.fetch(trefleID: trefleId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user