#lang racket (provide nutrient-value? maybe-nutrient-value? nutrient-value-hash/c (contract-out [insert-nutrient-values (-> connection? db-id? nutrient-value-hash/c (listof (cons/c symbol? any/c)))] [get-sorted-nutrient-values (-> nutrient-value-hash/c (listof (cons/c nutrient? nutrient-value?)))] [update-nutrient-values! (-> connection? db-id? nutrient-value-hash/c void?)] [residuals->nutrient-value-hash (-> (listof residual-vector/c) nutrient-value-hash/c)])) (require db sql racket/hash "nutrient.rkt" "utils.rkt") (define nutrient-value? (and/c real? (>=/c 0))) (define maybe-nutrient-value? (or/c nutrient-value? #f)) (define nutrient-value-hash/c (hash/c nutrient? nutrient-value? #:immutable #t)) ;; vector/c id, canonical name, french name, nutrient formula, value (ppm) (define residual-vector/c (vector/c db-id? string? string? string? real?)) (define (insert-nutrient-values conn nvs-id nutrient-values) (define nv-rows (for/list ([(n v) (in-hash nutrient-values)]) (map value->scalar-expr-ast (list nvs-id (nutrient-id n) v)))) (define result (query conn (insert #:into nutrient_values #:columns value_set_id nutrient_id value_ppm #:from (TableExpr:AST ,(make-values*-table-expr-ast nv-rows))))) (simple-result-info result)) (define (get-sorted-nutrient-values nv) (sort (hash->list (hash-filter-values nv positive?)) > #:key cdr)) (define (update-nutrient-values! conn nvs-id nutrient-values) (for ([(n v) (in-hash nutrient-values)]) (query-exec conn (update nutrient_values #:set [value_ppm ,v] #:where (and (= value_set_id ,nvs-id) (= nutrient_id ,(nutrient-id n))))))) (define (residuals->nutrient-value-hash residuals) (for/hash ([r (in-list residuals)]) (match-define (vector n-id n-canonical-name n-french-name n-formula value-ppm) r) (values (nutrient n-id n-canonical-name n-french-name n-formula) value-ppm))) (module+ test (require rackunit rackunit/text-ui db sql "../db/conn.rkt" "../db/migrations.rkt" "../models/nutrient.rkt") (run-tests (test-suite "Nutrient value model" #:before (λ () (connect! #:path 'memory) (migrate-all!) (create-nutrient! "Examplium" "Examplium" "Ex") (create-nutrient! "Ignorium" "Ignorium" "Ig") (create-nutrient! "Testium" "Testium" "Ts") (create-nutrient! "Zeroium" "Zeroium" "Zr")) #:after (λ () (disconnect!)) #;(test-case "Insert nutrient values" (define ex (get-nutrient #:name "Examplium")) (define ig (get-nutrient #:name "Ignorium")) ;; Create a nutrient_value_set for testing (define nvs-id (insert-id (query (current-conn) (insert #:into nutrient_value_sets #:set [nutrient_measurement_id ,sql-null] [crop_requirement_id ,sql-null] [fertilizer_product_id ,sql-null])))) (define nv-hash (hash ex 100.5 ig 50.25)) (define result (insert-nutrient-values (current-conn) nvs-id nv-hash)) (check-true (list? result)) (check-true (assoc 'insert-id result)) ;; Verify values were inserted (define ex-value (query-value (current-conn) (select value_ppm #:from nutrient_values #:where (and (= value_set_id ,nvs-id) (= nutrient_id ,(nutrient-id ex)))))) (check-= ex-value 100.5 0.001)) (test-case "Get sorted nutrient values filters positive and sorts descending" (define ex (get-nutrient #:name "Examplium")) (define ig (get-nutrient #:name "Ignorium")) (define ts (get-nutrient #:name "Testium")) (define zr (get-nutrient #:name "Zeroium")) (define nv-hash (hash ex 100 ig 50 ts 150 zr 0)) (define sorted (get-sorted-nutrient-values nv-hash)) ;; Should exclude zero values (check-equal? (length sorted) 3) ;; Should be sorted descending by value (check-equal? (cdr (first sorted)) 150) ; Testium (check-equal? (cdr (second sorted)) 100) ; Examplium (check-equal? (cdr (third sorted)) 50) ; Ignorium ;; Zeroium should not be in the list (check-false (member zr (map car sorted)))) (test-case "Get sorted nutrient values with all zeros returns empty" (define ex (get-nutrient #:name "Examplium")) (define ig (get-nutrient #:name "Ignorium")) (define nv-hash (hash ex 0 ig 0)) (define sorted (get-sorted-nutrient-values nv-hash)) (check-equal? (length sorted) 0)) (test-case "Get sorted nutrient values with negatives excluded" (define ex (get-nutrient #:name "Examplium")) (define ig (get-nutrient #:name "Ignorium")) ;; Note: nutrient-value? contract should prevent this, but testing the function (define nv-hash (hash ex 100 ig -50)) (define sorted (get-sorted-nutrient-values nv-hash)) (check-equal? (length sorted) 1) (check-equal? (cdr (first sorted)) 100)) #;(test-case "Update nutrient values" (define ex (get-nutrient #:name "Examplium")) (define ig (get-nutrient #:name "Ignorium")) ;; Create initial values (define nvs-id (insert-id (query (current-conn) (insert #:into nutrient_value_sets #:set [nutrient_measurement_id ,sql-null] [crop_requirement_id ,sql-null] [fertilizer_product_id ,sql-null])))) (define initial-hash (hash ex 100 ig 50)) (insert-nutrient-values (current-conn) nvs-id initial-hash) ;; Update values (define updated-hash (hash ex 200 ig 75)) (update-nutrient-values! (current-conn) nvs-id updated-hash) ;; Verify updates (define ex-value (query-value (current-conn) (select value_ppm #:from nutrient_values #:where (and (= value_set_id ,nvs-id) (= nutrient_id ,(nutrient-id ex)))))) (check-= ex-value 200 0.001) (define ig-value (query-value (current-conn) (select value_ppm #:from nutrient_values #:where (and (= value_set_id ,nvs-id) (= nutrient_id ,(nutrient-id ig)))))) (check-= ig-value 75 0.001)) (test-case "Residuals to nutrient value hash conversion" (define ex (get-nutrient #:name "Examplium")) (define ig (get-nutrient #:name "Ignorium")) (define residuals (list (vector (nutrient-id ex) "Examplium" "Examplium" "Ex" 123.45) (vector (nutrient-id ig) "Ignorium" "Ignorium" "Ig" 67.89))) (define nv-hash (residuals->nutrient-value-hash residuals)) (check-equal? (hash-count nv-hash) 2) (check-= (hash-ref nv-hash ex) 123.45 0.001) (check-= (hash-ref nv-hash ig) 67.89 0.001)) (test-case "Residuals to nutrient value hash with empty list" (define nv-hash (residuals->nutrient-value-hash '())) (check-true (hash-empty? nv-hash))) (test-case "Residuals to nutrient value hash preserves all fields" (define residuals (list (vector 1 "Examplium" "Examplium-FR" "Ex" 100.0))) (define nv-hash (residuals->nutrient-value-hash residuals)) (define nutrient-key (car (hash-keys nv-hash))) (check-equal? (nutrient-id nutrient-key) 1) (check-equal? (nutrient-canonical-name nutrient-key) "Examplium") (check-equal? (nutrient-french-name nutrient-key) "Examplium-FR") (check-equal? (nutrient-formula nutrient-key) "Ex") (check-= (hash-ref nv-hash nutrient-key) 100.0 0.001)))))