From 6a0020701b6922b6a1738632e6186a283478bb01 Mon Sep 17 00:00:00 2001 From: Thad Kerosky Date: Wed, 23 Oct 2024 00:15:39 +0000 Subject: [PATCH] Refactor zod parsing and types for typecheck Co-authored-by: Charlie Kelly Co-authored-by: plocket Co-authored-by: thomas-davis Co-authored-by: TBardini --- .../CaseSummaryComponents/AnalysisHeader.tsx | 1 + .../EnergyUseHistoryChart.tsx | 6 +- heat-stack/app/routes/_heat+/single.tsx | 18 +- heat-stack/types/index.ts | 177 ++++++++---------- heat-stack/types/types.ts | 15 +- 5 files changed, 95 insertions(+), 122 deletions(-) diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/AnalysisHeader.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/AnalysisHeader.tsx index 755a9d48..d6007ab5 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/AnalysisHeader.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/AnalysisHeader.tsx @@ -38,6 +38,7 @@ export function AnalysisHeader({ usage_data }: { usage_data: UsageDataSchema}) { // Calculate the number of billing periods included in Heating calculations const heatingAnalysisTypeRecords = usage_data?.billing_records?.filter( (billingRecord) => billingRecord.analysis_type === 1, + // Do wee need this code instead? (billingRecord) => billingRecord.analysis_type !== "NOT_ALLOWED_IN_CALCULATIONS", ); const recordsIncludedByDefault = heatingAnalysisTypeRecords?.filter( diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx index 202fc43d..e8c1da81 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx @@ -1,10 +1,6 @@ import { useState, useEffect } from 'react' import { type z } from 'zod' -import { type UsageDataSchema, type BillingRecordsSchema } from '#/types/types.ts'; -import { - NaturalGasUsageData, - type NaturalGasBillRecord as NaturalGasBillRecordZod, -} from '#types/index' +import { type UsageDataSchema, type BillingRecordsSchema } from '#/types/types.ts' import { Checkbox } from '../../../../components/ui/checkbox.tsx' import { diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index 360ccf28..8b1b9ab3 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -52,7 +52,8 @@ import WeatherUtil from '#app/utils/WeatherUtil' // - [ ] Will weather service take timestamp instead of timezone date data? // Ours -import { Home, Location, Case, type NaturalGasUsageData, /* validateNaturalGasUsageData, HeatLoadAnalysisZod */ } from '../../../types/index.ts' +import { HomeSchema, LocationSchema, CaseSchema /* validateNaturalGasUsageData, HeatLoadAnalysisZod */ } from '../../../types/index.ts' +import { type NaturalGasUsageDataSchema} from '../../../types/types.ts' import { CurrentHeatingSystem } from '../../components/ui/heat/CaseSummaryComponents/CurrentHeatingSystem.tsx' import { EnergyUseHistory } from '../../components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx' import { HomeInformation } from '../../components/ui/heat/CaseSummaryComponents/HomeInformation.tsx' @@ -61,11 +62,11 @@ import HeatLoadAnalysis from './heatloadanalysis.tsx' /** Modeled off the conform example at * https://github.com/epicweb-dev/web-forms/blob/b69e441f5577b91e7df116eba415d4714daacb9d/exercises/03.schema-validation/03.solution.conform-form/app/routes/users%2B/%24username_%2B/notes.%24noteId_.edit.tsx#L48 */ -const HomeFormSchema = Home.pick({ living_area: true }) - .and(Location.pick({ address: true })) - .and(Case.pick({ name: true })) +const HomeFormSchema = HomeSchema.pick({ living_area: true }) + .and(LocationSchema.pick({ address: true })) + .and(CaseSchema.pick({ name: true })) -const CurrentHeatingSystemSchema = Home.pick({ +const CurrentHeatingSystemSchema = HomeSchema.pick({ fuel_type: true, heating_system_efficiency: true, design_temperature_override: true, @@ -251,8 +252,7 @@ export async function action({ request, params }: ActionFunctionArgs) { */ // This assignment of the same name is a special thing. We don't remember the name right now. // It's not necessary, but it is possible. - type NaturalGasUsageData = z.infer; - const pyodideResultsFromTextFile: NaturalGasUsageData = executeParseGasBillPy(uploadedTextFile).toJs() + const pyodideResultsFromTextFile: NaturalGasUsageDataSchema = executeParseGasBillPy(uploadedTextFile).toJs() // console.log('result', pyodideResultsFromTextFile )//, validateNaturalGasUsageData(pyodideResultsFromTextFile)) const startDateString = pyodideResultsFromTextFile.get('overall_start_date'); @@ -535,7 +535,6 @@ export default function Inputs() { } let usage_data = null; - let modifiedLastResult = null; let show_usage_data = lastResult !== undefined; console.log('lastResult', lastResult) @@ -547,8 +546,7 @@ export default function Inputs() { const parsedData = JSON.parse(lastResult.data); // Recursively transform any Maps in lastResult to objects - modifiedLastResult = replacedMapToObject(parsedData); - usage_data = modifiedLastResult; // Get the relevant part of the transformed result + usage_data = replacedMapToObject(parsedData); // Get the relevant part of the transformed result console.log('usage_data', usage_data) } catch (error) { diff --git a/heat-stack/types/index.ts b/heat-stack/types/index.ts index 8202222d..b934a472 100644 --- a/heat-stack/types/index.ts +++ b/heat-stack/types/index.ts @@ -1,28 +1,12 @@ -import { z } from 'zod'; +import { number, z } from 'zod'; +export type NaturalGasUsageDataSchema = z.infer; // JS team wants to discuss this name -export const Case = z.object({ +export const CaseSchema = z.object({ name: z.string() }) -/** TODO: fix camelcase and snake case mix */ -export const HeatLoadAnalysisZod = z.object({ - rulesEngineVersion: z.string(), - estimatedBalancePoint: z.number(), - otherFuelUsage: z.number(), - averageIndoorTemperature: z.number(), - differenceBetweenTiAndTbp: z.number(), - /** - * designTemperature in Fahrenheit - */ - design_temperature: z.number().max(50).min(-50), - wholeHomeHeatLossRate: z.number(), - standardDeviationHeatLossRate: z.number(), - averageHeatLoad: z.number(), - maximumHeatLoad: z.number(), -}); - -export const Home = z.object({ +export const HomeSchema = z.object({ /** * unit: square feet */ @@ -41,69 +25,29 @@ export const Home = z.object({ standByLosses: z.number(), }); -export const Location = z.object({ +export const LocationSchema = z.object({ address: z.string(), }); -export const NaturalGasBill = z.object({ - provider: z.string(), -}); +// Not used +// export const NaturalGasBill = z.object({ +// provider: z.string(), +// }); -export const NaturalGasBillRecord = z.object({ - periodStartDate: z.date(), - periodEndDate: z.date(), - usageTherms: z.number(), - inclusionOverride: z.enum(["Include", "Do not include", "Include in other analysis"]), - }); - - // Helper function to create a date string schema - const dateStringSchema = () => - z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Invalid date format. Use YYYY-MM-DD"); - - export const NaturalGasUsageData = z.map( - z.enum(["overall_start_date", "overall_end_date", "records"]), - z.union([ - dateStringSchema(), - z.array(NaturalGasBillRecord) - ]) - ); - - -// Convert Map to plain object (recursive) -/** TODO: make sure this is how we need it to be for Map validation */ -export function mapToObject(map: Map): any { - const obj = Object.fromEntries(map); - for (let key in obj) { - if (obj[key] instanceof Map) { - obj[key] = mapToObject(obj[key]); - } else if (Array.isArray(obj[key])) { - obj[key] = obj[key].map((item: any) => - item instanceof Map ? mapToObject(item) : item - ); - } - } - return obj; - } - type NaturalGasUsageData = z.infer; - // Validation function - export const validateNaturalGasUsageData = (data: unknown): NaturalGasUsageData => { - const plainObject = data instanceof Map ? mapToObject(data) : data; - return NaturalGasUsageData.parse(plainObject); - }; - - -export const OilPropaneBill = z.object({ - provider: z.string(), - precedingDeliveryDate: z.date(), -}); +// Not used +// export const OilPropaneBill = z.object({ +// provider: z.string(), +// precedingDeliveryDate: z.date(), +// }); -export const OilPropaneBillRecord = z.object({ - periodStartDate: z.date(), - periodEndDate: z.date(), - gallons: z.number(), - inclusionOverride: z.enum(['Include', 'Do not include', 'Include in other analysis']), -}); +// Not used +// export const OilPropaneBillRecord = z.object({ +// periodStartDate: z.date(), +// periodEndDate: z.date(), +// gallons: z.number(), +// inclusionOverride: z.enum(['Include', 'Do not include', 'Include in other analysis']), +// }); // Define the schema for balance records export const balancePointGraphRecordSchema = z.object({ @@ -121,37 +65,68 @@ export const balancePointGraphSchema = z.object({ // Define the schema for the 'summary_output' key export const summaryOutputSchema = z.object({ - estimated_balance_point: z.number().optional().default(0), - other_fuel_usage: z.number().optional().default(0), - average_indoor_temperature: z.number().optional().default(0), - difference_between_ti_and_tbp: z.number().optional().default(0), - design_temperature: z.number().optional().default(0), - whole_home_heat_loss_rate: z.number().optional().default(0), - standard_deviation_of_heat_loss_rate: z.number().optional().default(0), - average_heat_load: z.number().optional().default(0), - maximum_heat_load: z.number().optional().default(0), + // rulesEngineVersion: z.string(), // TODO + estimated_balance_point: z.number(), + other_fuel_usage: z.number(), + average_indoor_temperature: z.number(), + difference_between_ti_and_tbp: z.number(), + /** + * designTemperature in Fahrenheit + */ + design_temperature: z.number().max(50).min(-50), + whole_home_heat_loss_rate: z.number(), + standard_deviation_of_heat_loss_rate: z.number(), + average_heat_load: z.number(), + maximum_heat_load: z.number(), }); - -// Define the schema for billing records -export const billingRecordSchema = z.object({ - period_start_date: z.string().default(''), - period_end_date: z.string().default(''), - usage: z.number().default(0), - analysis_type_override: z.any().nullable(), - inclusion_override: z.boolean().default(false), - analysis_type: z.number().default(0), - default_inclusion_by_calculation: z.boolean().default(false), - eliminated_as_outlier: z.boolean().default(false), - whole_home_heat_loss_rate: z.number().optional().default(0), +export const NaturalGasBillRecord = z.object({ + periodStartDate: z.date(), + periodEndDate: z.date(), + usageTherms: z.number(), + inclusionOverride: z.number(), + // inclusionOverride: z.enum(["Include", "Do not include", "Include in other analysis"]), +}); + +// Helper function to create a date string schema +const dateStringSchema = () => + z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Invalid date format. Use YYYY-MM-DD') + +export const naturalGasUsageSchema = z.map( + z.enum(['overall_start_date', 'overall_end_date', 'records']), + z.union([dateStringSchema(), z.array(NaturalGasBillRecord)]), +) + +// Define the schema for one billing record +export const oneBillingRecordSchema = z.object({ + period_start_date: z.string(), + period_end_date: z.string(), + usage: z.number(), + /** Ask the rules engine if this is an enum: + z.enum(["Include", "Do not include", "Include in other analysis"]), + What does "Include in other analysis" mean? + Keep this as default `false`? */ + inclusion_override: z.boolean(), + /** + * ALLOWED_HEATING_USAGE is for winter — red + * + * ALLOWED_NON_HEATING_USAGE is for summer — blue + * + * NOT_ALLOWED_IN_CALCULATIONS is for "shoulder" months/seasons — crossed out + */ + // analysis_type: z.enum(["ALLOWED_HEATING_USAGE", "ALLOWED_NON_HEATING_USAGE", "NOT_ALLOWED_IN_CALCULATIONS"]), + analysis_type: z.number(), + default_inclusion_by_calculation: z.boolean(), + eliminated_as_outlier: z.boolean(), + whole_home_heat_loss_rate: z.number(), }); -// Define the schema for 'billing_records' key -export const billingRecordsSchema = z.array(billingRecordSchema); +// Define the schema for the 'billing_records' list +export const allBillingRecordsSchema = z.array(oneBillingRecordSchema); // Define the schema for the 'usage_data' key export const usageDataSchema = z.object({ summary_output: summaryOutputSchema, balance_point_graph: balancePointGraphSchema, - billing_records: billingRecordsSchema, + billing_records: allBillingRecordsSchema, }) diff --git a/heat-stack/types/types.ts b/heat-stack/types/types.ts index 1cd2f32f..ba6039d8 100644 --- a/heat-stack/types/types.ts +++ b/heat-stack/types/types.ts @@ -3,14 +3,17 @@ import { type balancePointGraphRecordSchema, type balancePointGraphSchema, type summaryOutputSchema, - type billingRecordSchema, - type billingRecordsSchema, + type oneBillingRecordSchema, + type allBillingRecordsSchema, type usageDataSchema, + type naturalGasUsageSchema } from './index.ts' -export type BalancePointGraphRecordSchema = z.infer; -export type BalancePointGraphSchema = z.infer; + +export type NaturalGasUsageDataSchema = z.infer; +export type BalancePoointGraphRecordSchema = z.infer; +export type BalancePintGraphSchema = z.infer; export type SummaryOutputSchema = z.infer; -export type BillingRecordSchema = z.infer; -export type BillingRecordsSchema = z.infer; +export type BillingRecordSchema = z.infer; +export type BillingRecordsSchema = z.infer; export type UsageDataSchema = z.infer; \ No newline at end of file