From dde8950e6bad3941e4a16ad019c4097eb3bec740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hallvard=20Tr=C3=A6tteberg?= Date: Fri, 20 Feb 2026 10:47:23 +0100 Subject: [PATCH 1/3] support yaml, making it easier to contribute --- content/collections/collectors-teeing.json | 50 ---------------------- content/collections/collectors-teeing.yaml | 49 +++++++++++++++++++++ html-generators/generate.java | 32 +++++++++----- html-generators/generate.py | 11 ++++- 4 files changed, 79 insertions(+), 63 deletions(-) delete mode 100644 content/collections/collectors-teeing.json create mode 100644 content/collections/collectors-teeing.yaml diff --git a/content/collections/collectors-teeing.json b/content/collections/collectors-teeing.json deleted file mode 100644 index bf4b134..0000000 --- a/content/collections/collectors-teeing.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "id": 26, - "slug": "collectors-teeing", - "title": "Collectors.teeing()", - "category": "collections", - "difficulty": "intermediate", - "jdkVersion": "12", - "oldLabel": "Java 8", - "modernLabel": "Java 12+", - "oldApproach": "Two Passes", - "modernApproach": "teeing()", - "oldCode": "long count = items.stream().count();\ndouble sum = items.stream()\n .mapToDouble(Item::price)\n .sum();\nvar result = new Stats(count, sum);", - "modernCode": "var result = items.stream().collect(\n Collectors.teeing(\n Collectors.counting(),\n Collectors.summingDouble(Item::price),\n Stats::new\n )\n);", - "summary": "Compute two aggregations in a single stream pass.", - "explanation": "Collectors.teeing() sends each element to two downstream collectors and merges the results. This avoids streaming the data twice or using a mutable accumulator.", - "whyModernWins": [ - { - "icon": "⚡", - "title": "Single pass", - "desc": "Process the stream once instead of twice." - }, - { - "icon": "🧩", - "title": "Composable", - "desc": "Combine any two collectors with a merger function." - }, - { - "icon": "🔒", - "title": "Immutable result", - "desc": "Merge into a record or value object directly." - } - ], - "support": { - "state": "available", - "description": "Widely available since JDK 12 (March 2019)" - }, - "prev": "collections/sequenced-collections", - "next": "collections/stream-toarray-typed", - "related": [ - "collections/copying-collections-immutably", - "collections/unmodifiable-collectors", - "collections/stream-toarray-typed" - ], - "docs": [ - { - "title": "Collectors.teeing()", - "href": "https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/stream/Collectors.html#teeing(java.util.stream.Collector,java.util.stream.Collector,java.util.function.BiFunction)" - } - ] -} diff --git a/content/collections/collectors-teeing.yaml b/content/collections/collectors-teeing.yaml new file mode 100644 index 0000000..5fa6ffd --- /dev/null +++ b/content/collections/collectors-teeing.yaml @@ -0,0 +1,49 @@ +id: 26 +slug: collectors-teeing +title: Collectors.teeing() +category: collections +difficulty: intermediate +jdkVersion: "12" +oldLabel: Java 8 +modernLabel: Java 12+ +oldApproach: Two Passes +modernApproach: teeing() +oldCode: | + // python + long count = items.stream().count(); + double sum = items.stream() + .mapToDouble(Item::price) + .sum(); + var result = new Stats(count, sum); +modernCode: | + var result = items.stream().collect( + Collectors.teeing( + Collectors.counting(), + Collectors.summingDouble(Item::price), + Stats::new + ) + ); +summary: Compute two aggregations in a single stream pass. +explanation: Collectors.teeing() sends each element to two downstream collectors and merges the results. This avoids streaming the data twice or using a mutable accumulator. +whyModernWins: + - icon: ⚡ + title: Single pass + desc: Process the stream once instead of twice. + - icon: 🧩 + title: Composable + desc: Combine any two collectors with a merger function. + - icon: 🔒 + title: Immutable result + desc: Merge into a record or value object directly. +support: + state: available + description: Widely available since JDK 12 (March 2019) +prev: collections/sequenced-collections +next: collections/stream-toarray-typed +related: + - collections/copying-collections-immutably + - collections/unmodifiable-collectors + - collections/stream-toarray-typed +docs: + - title: Collectors.teeing() + href: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/stream/Collectors.html#teeing(java.util.stream.Collector,java.util.stream.Collector,java.util.function.BiFunction) diff --git a/html-generators/generate.java b/html-generators/generate.java index f927f1a..654b2f3 100644 --- a/html-generators/generate.java +++ b/html-generators/generate.java @@ -1,10 +1,12 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? //JAVA 25 //DEPS com.fasterxml.jackson.core:jackson-databind:2.18.3 +//DEPS com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.18.3 import module java.base; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; /** * Generate HTML detail pages from JSON snippet files and slug-template.html. @@ -14,7 +16,13 @@ static final String CONTENT_DIR = "content"; static final String SITE_DIR = "site"; static final Pattern TOKEN = Pattern.compile("\\{\\{(\\w+)}}"); -static final ObjectMapper MAPPER = new ObjectMapper(); +static final ObjectMapper JSON_MAPPER = new ObjectMapper(); +static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); +static final Map MAPPERS = Map.of( + "json", JSON_MAPPER, + "yaml", YAML_MAPPER, + "yml", YAML_MAPPER +); static final String CATEGORIES_FILE = "html-generators/categories.properties"; static final SequencedMap CATEGORY_DISPLAY = loadCategoryDisplay(); @@ -100,7 +108,7 @@ void main() throws IOException { // Rebuild data/snippets.json var snippetsList = allSnippets.values().stream() .map(s -> { - Map map = MAPPER.convertValue(s.node(), new TypeReference>() {}); + Map map = JSON_MAPPER.convertValue(s.node(), new TypeReference>() {}); EXCLUDED_KEYS.forEach(map::remove); return map; }) @@ -127,14 +135,16 @@ SequencedMap loadAllSnippets() throws IOException { for (var cat : CATEGORY_DISPLAY.sequencedKeySet()) { var catDir = Path.of(CONTENT_DIR, cat); if (!Files.isDirectory(catDir)) continue; - try (var stream = Files.newDirectoryStream(catDir, "*.json")) { - var sorted = new ArrayList(); - stream.forEach(sorted::add); - sorted.sort(Path::compareTo); - for (var path : sorted) { - var snippet = new Snippet(MAPPER.readTree(path.toFile())); - snippets.put(snippet.key(), snippet); - } + for (var ext : MAPPERS.keySet()) { + try (var stream = Files.newDirectoryStream(catDir, "*." + ext)) { + var sorted = new ArrayList(); + stream.forEach(sorted::add); + sorted.sort(Path::compareTo); + for (var path : sorted) { + var snippet = new Snippet(MAPPERS.get(ext).readTree(path.toFile())); + snippets.put(snippet.key(), snippet); + } + } } } return snippets; @@ -145,7 +155,7 @@ String escape(String text) { } String jsonEscape(String text) throws IOException { - var quoted = MAPPER.writeValueAsString(text); + var quoted = JSON_MAPPER.writeValueAsString(text); var inner = quoted.substring(1, quoted.length() - 1); var sb = new StringBuilder(inner.length()); for (int i = 0; i < inner.length(); i++) { diff --git a/html-generators/generate.py b/html-generators/generate.py index b9f2739..2263603 100644 --- a/html-generators/generate.py +++ b/html-generators/generate.py @@ -2,6 +2,7 @@ """Generate HTML detail pages from JSON snippet files and slug-template.html.""" import json +import yaml import glob import os import html @@ -73,10 +74,16 @@ def load_all_snippets(): snippets = {} json_files = [] for cat in CATEGORY_DISPLAY: - json_files.extend(sorted(glob.glob(f"{CONTENT_DIR}/{cat}/*.json"))) + json_files.extend(glob.glob(f"{CONTENT_DIR}/{cat}/*.json")) + json_files.extend(glob.glob(f"{CONTENT_DIR}/{cat}/*.yaml")) + json_files.extend(glob.glob(f"{CONTENT_DIR}/{cat}/*.yml")) + json_files.sort() for path in json_files: with open(path) as f: - data = json.load(f) + if path.endswith(".yaml") or path.endswith(".yml"): + data = yaml.safe_load(f) + else: + data = json.load(f) key = f"{data['category']}/{data['slug']}" data["_path"] = key snippets[key] = data From ad084788a692b00ecc26f985639d7934bcdd1907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hallvard=20Tr=C3=A6tteberg?= Date: Fri, 20 Feb 2026 10:52:00 +0100 Subject: [PATCH 2/3] removed line used for testing --- content/collections/collectors-teeing.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/content/collections/collectors-teeing.yaml b/content/collections/collectors-teeing.yaml index 5fa6ffd..368523d 100644 --- a/content/collections/collectors-teeing.yaml +++ b/content/collections/collectors-teeing.yaml @@ -9,7 +9,6 @@ modernLabel: Java 12+ oldApproach: Two Passes modernApproach: teeing() oldCode: | - // python long count = items.stream().count(); double sum = items.stream() .mapToDouble(Item::price) From 94188af7c04bb984572646a68228d6a0f0317cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hallvard=20Tr=C3=A6tteberg?= Date: Fri, 20 Feb 2026 11:01:07 +0100 Subject: [PATCH 3/3] fix sorting across extensions --- html-generators/generate.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/html-generators/generate.java b/html-generators/generate.java index 654b2f3..7563556 100644 --- a/html-generators/generate.java +++ b/html-generators/generate.java @@ -135,17 +135,21 @@ SequencedMap loadAllSnippets() throws IOException { for (var cat : CATEGORY_DISPLAY.sequencedKeySet()) { var catDir = Path.of(CONTENT_DIR, cat); if (!Files.isDirectory(catDir)) continue; + var sorted = new ArrayList(); + // first collect and sortall files for (var ext : MAPPERS.keySet()) { try (var stream = Files.newDirectoryStream(catDir, "*." + ext)) { - var sorted = new ArrayList(); stream.forEach(sorted::add); - sorted.sort(Path::compareTo); - for (var path : sorted) { - var snippet = new Snippet(MAPPERS.get(ext).readTree(path.toFile())); - snippets.put(snippet.key(), snippet); - } } } + sorted.sort(Path::compareTo); + for (var path : sorted) { + var filename = path.getFileName().toString(); + var ext = filename.substring(filename.lastIndexOf('.') + 1); + var json = MAPPERS.get(ext).readTree(path.toFile()); + var snippet = new Snippet(json); + snippets.put(snippet.key(), snippet); + } } return snippets; }