Skip to main content

data_transformation/
data_transformation.rs

1//! Data Transformation Example
2//!
3//! Demonstrates an ETL-style pipeline using Functor (transforms),
4//! Chain/do_! (parsing), Lens (field access), and Foldable + Monoid (aggregation).
5//!
6//! Run with: cargo run -p karpal-std --example data_transformation
7
8use karpal_std::prelude::*;
9
10// --- Domain types ---
11
12#[derive(Debug, Clone)]
13struct RawRecord {
14    id: String,
15    name: String,
16    amount: String,
17    category: String,
18}
19
20#[derive(Debug, Clone)]
21struct Transaction {
22    id: u32,
23    name: String,
24    amount_cents: i64,
25    category: String,
26}
27
28#[derive(Debug, Clone)]
29struct Summary {
30    count: i64,
31    total_cents: i64,
32}
33
34impl Semigroup for Summary {
35    fn combine(self, other: Self) -> Self {
36        Summary {
37            count: self.count + other.count,
38            total_cents: self.total_cents + other.total_cents,
39        }
40    }
41}
42
43impl Monoid for Summary {
44    fn empty() -> Self {
45        Summary {
46            count: 0,
47            total_cents: 0,
48        }
49    }
50}
51
52// --- Parsing with do_! (Chain) ---
53
54/// Parse a raw record into a Transaction, failing on bad data.
55fn parse_record(raw: RawRecord) -> Option<Transaction> {
56    let name = raw.name.clone();
57    let category = raw.category.clone();
58    do_! { OptionF;
59        id = raw.id.parse::<u32>().ok();
60        amount = raw.amount.parse::<f64>().ok();
61        Some(Transaction {
62            id,
63            name: name.clone(),
64            amount_cents: (amount * 100.0) as i64,
65            category: category.clone(),
66        })
67    }
68}
69
70// --- Lens for field access ---
71
72fn amount_lens() -> SimpleLens<Transaction, i64> {
73    Lens::new(
74        |t: &Transaction| t.amount_cents,
75        |t, amount_cents| Transaction { amount_cents, ..t },
76    )
77}
78
79fn name_lens() -> SimpleLens<Transaction, String> {
80    Lens::new(
81        |t: &Transaction| t.name.clone(),
82        |t, name| Transaction { name, ..t },
83    )
84}
85
86// --- Functor: transform fields ---
87
88/// Apply a discount: reduce amount by a percentage.
89fn apply_discount(transactions: Vec<Transaction>, pct: f64) -> Vec<Transaction> {
90    let lens = amount_lens();
91    VecF::fmap(transactions, |t| {
92        lens.over(t, |a| (a as f64 * (1.0 - pct / 100.0)) as i64)
93    })
94}
95
96/// Normalize names to uppercase.
97fn normalize_names(transactions: Vec<Transaction>) -> Vec<Transaction> {
98    let lens = name_lens();
99    VecF::fmap(transactions, |t| lens.over(t, |n| n.to_uppercase()))
100}
101
102// --- Foldable + Monoid: aggregate ---
103
104/// Summarize a collection of transactions.
105fn summarize(transactions: &[Transaction]) -> Summary {
106    VecF::fold_map(transactions.to_vec(), |t| Summary {
107        count: 1,
108        total_cents: t.amount_cents,
109    })
110}
111
112/// Summarize by category.
113fn summarize_by_category(transactions: &[Transaction]) -> Vec<(String, Summary)> {
114    let mut categories: Vec<String> = transactions.iter().map(|t| t.category.clone()).collect();
115    categories.sort();
116    categories.dedup();
117
118    categories
119        .into_iter()
120        .map(|cat| {
121            let filtered: Vec<Transaction> = transactions
122                .iter()
123                .filter(|t| t.category == cat)
124                .cloned()
125                .collect();
126            (cat, summarize(&filtered))
127        })
128        .collect()
129}
130
131// --- Traversable: parse all-or-nothing ---
132
133fn parse_all(records: Vec<RawRecord>) -> Option<Vec<Transaction>> {
134    VecF::traverse::<OptionF, _, _, _>(records, parse_record)
135}
136
137fn main() {
138    println!("=== Data Transformation Example ===\n");
139
140    // Sample data
141    let records = vec![
142        RawRecord {
143            id: "1".into(),
144            name: "Alice".into(),
145            amount: "99.99".into(),
146            category: "electronics".into(),
147        },
148        RawRecord {
149            id: "2".into(),
150            name: "Bob".into(),
151            amount: "24.50".into(),
152            category: "books".into(),
153        },
154        RawRecord {
155            id: "3".into(),
156            name: "Carol".into(),
157            amount: "149.00".into(),
158            category: "electronics".into(),
159        },
160        RawRecord {
161            id: "4".into(),
162            name: "Dave".into(),
163            amount: "12.75".into(),
164            category: "books".into(),
165        },
166    ];
167
168    // 1. Parse all records (Traversable)
169    println!("--- Parse records (Traversable) ---");
170    let transactions = parse_all(records).expect("All records should parse");
171    for t in &transactions {
172        println!(
173            "  #{}: {} - ${:.2} ({})",
174            t.id,
175            t.name,
176            t.amount_cents as f64 / 100.0,
177            t.category
178        );
179    }
180
181    // 2. Transform with Functor + Lens
182    println!("\n--- Apply 10% discount (Functor + Lens) ---");
183    let discounted = apply_discount(transactions.clone(), 10.0);
184    for t in &discounted {
185        println!("  #{}: ${:.2}", t.id, t.amount_cents as f64 / 100.0);
186    }
187
188    println!("\n--- Normalize names (Functor + Lens) ---");
189    let normalized = normalize_names(transactions.clone());
190    for t in &normalized {
191        println!("  #{}: {}", t.id, t.name);
192    }
193
194    // 3. Aggregate with Foldable + Monoid
195    println!("\n--- Overall summary (Foldable + Monoid) ---");
196    let summary = summarize(&transactions);
197    println!(
198        "  {} transactions, total: ${:.2}",
199        summary.count,
200        summary.total_cents as f64 / 100.0
201    );
202
203    println!("\n--- By category ---");
204    for (cat, s) in summarize_by_category(&transactions) {
205        println!(
206            "  {}: {} transactions, total: ${:.2}",
207            cat,
208            s.count,
209            s.total_cents as f64 / 100.0
210        );
211    }
212
213    // 4. Failed parse demonstration
214    println!("\n--- Failed parse (bad data) ---");
215    let bad_records = vec![
216        RawRecord {
217            id: "5".into(),
218            name: "Eve".into(),
219            amount: "50.00".into(),
220            category: "food".into(),
221        },
222        RawRecord {
223            id: "bad".into(),
224            name: "Frank".into(),
225            amount: "30.00".into(),
226            category: "food".into(),
227        },
228    ];
229    println!("  parse_all result: {:?}", parse_all(bad_records));
230}