Published 2026-07-03
Generate CSV and XML files for store opening hours signs from JSON input data.

This Dart command-line tool processes store opening hours data and generates:
Perfect for:
✅ Multi-Country Support: NL, FR, DE, AT, PL, SK, IT, ES
✅ Country-Specific Formatting: Automatic time format adaptation
✅ Comprehensive Validation: Catches data errors before processing
✅ Detailed Error Reporting: Know exactly what went wrong and where
✅ Batch Processing: Handle hundreds of stores efficiently
✅ ISO Week Support: Proper week number calculations
✅ Address Parsing: International address format support
✅ Audit Trail: Complete processing reports with statistics
macOS (using Homebrew):
brew tap dart-lang/dart
brew install dart
Linux:
sudo apt-get update
sudo apt-get install apt-transport-https
wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo gpg --dearmor -o /usr/share/keyrings/dart.gpg
echo 'deb [signed-by=/usr/share/keyrings/dart.gpg] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
sudo apt-get update
sudo apt-get install dart
Windows (using Chocolatey):
choco install dart-sdk
# Clone repository
git clone <repository-url>
cd dart_action_openingstijdenbordjes_convert_to_csv_and_xml
# Get dependencies
dart pub get
dart run bin/dart_action_openingstijdenbordjes_convert_to_csv_and_xml.dart --inputFile /path/to/input.json
dart run bin/dart_action_openingstijdenbordjes_convert_to_csv_and_xml.dart -i /path/to/input.json
All files are created in the same directory as your input file:
/path/to/input/directory/
├── csv_output/
│ ├── Week 49 - nl-NL - A4-poster openingstijden 2025.csv
│ ├── Week 49 - fr-FR - A4-poster openingstijden 2025.csv
│ └── ...
├── xml_output/
│ ├── 1001_Openingstijden week 49.xml
│ ├── 1002_Openingstijden week 49.xml
│ └── ...
└── processing_report_20251209_153045.txt
[
{
"store": "1001",
"name_sm": "John Doe",
"street": "Main Street",
"house_no": "123",
"house_no_suffix": "",
"postal_code": "1234 AB",
"city": "Amsterdam",
"language": "NL",
"country": "NL",
"monday": "08:00 - 18:00",
"tuesday": "08:00 - 18:00",
"wednesday": "08:00 - 18:00",
"thursday": "08:00 - 18:00",
"friday": "08:00 - 18:00",
"saturday": "08:00 - 17:00",
"sunday": "12:00 - 17:00",
"number_of_signs": 2,
"week": 49,
"year": 2025
}
]
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
store |
String | ✅ Yes | Store identification number | "1001", "7169" |
name_sm |
String | ❌ Optional | Store manager name | "John Doe", "" |
street |
String | ✅ Yes | Street name. If house_no is empty and the street starts with a number, that number is extracted automatically. |
"Main Street", "52 Rue Emile Zola" |
house_no |
String | ❌ Optional | House number. Defaults to 0 when empty and no leading street number can be extracted. |
"123", "", "15-17" |
house_no_suffix |
String | ❌ Optional | House number suffix or addition. | "", "A", "bis" |
postal_code |
String | ✅ Yes | Postal/ZIP code | "1234 AB", "5071" |
city |
String | ✅ Yes | City name | "Amsterdam" |
language |
String | ✅ Yes | Language code (uppercase) | "NL", "FR", "DE" |
country |
String | ✅ Yes | Country code (uppercase) | "NL", "AT", "FR" |
monday |
String | ⚠️ Special* | Monday hours | "08:00 - 18:00", "" |
tuesday |
String | ⚠️ Special* | Tuesday hours | "08:00 - 18:00", "" |
wednesday |
String | ⚠️ Special* | Wednesday hours | "08:00 - 18:00", "" |
thursday |
String | ⚠️ Special* | Thursday hours | "08:00 - 18:00", "" |
friday |
String | ⚠️ Special* | Friday hours | "08:00 - 18:00", "" |
saturday |
String | ⚠️ Special* | Saturday hours | "08:00 - 17:00", "" |
sunday |
String | ⚠️ Special* | Sunday hours | "12:00 - 17:00", "" |
number_of_signs |
Int/String | ❌ Optional | Number of signs to generate (defaults to 2) | 2, "3", "" |
week |
Int/String | ✅ Yes | ISO week number (1-53) | 49, "49" |
year |
Int/String | ✅ Yes | Year | 2025, "2025" |
⚠️ Special: Individual days can be empty (= closed), but at least ONE day must have hours
Address normalization:
"street": "52 Rue Emile Zola", "house_no": "" becomes street Rue Emile Zola with house number 52"street": "Rue Des Bas Buissons", "house_no": "" defaults house number to 0Location: csv_output/
Naming: Week {week} - {ISO} - A4-poster openingstijden {year}.csv
Purpose: Bulk data for printing systems
Format: UTF-16LE with BOM
Columns:
Filiaal,Naam bedrijfsleider,Straat,Huisnummer,Toevoeging,Postcode,Plaats,Maandag,Dinsdag,Woensdag,Donderdag,Vrijdag,Zaterdag,Zondag,Datum
Example Row:
1001,John Doe,Main Street,123,,1234 AB,Amsterdam,08:00 - 18:00,08:00 - 18:00,08:00 - 18:00,08:00 - 18:00,08:00 - 18:00,08:00 - 17:00,12:00 - 17:00,2025-12-01
Note: Each store generates 2 rows (for number_of_signs)
Location: xml_output/
Naming: {store}_Openingstijden week {week}.xml
Purpose: Individual store sign generation
Format: UTF-8 with BOM
Example:
<?xml version="1.0" encoding="UTF-8"?>
<order xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.w3.org/1999/xhtml">
<deliveryDate>2025-12-01</deliveryDate>
<deliveryAddress>
<companyNumber>1001</companyNumber>
<yourReference>1001 - Openingstijden week 49</yourReference>
<companyName>Action 1001</companyName>
<countryCode>nl-NL</countryCode>
<street>Main Street</street>
<houseNo>123</houseNo>
<houseNoSuffix></houseNoSuffix>
<postcode>1234 AB</postcode>
<city>Amsterdam</city>
<email>blackhole@huighaverlag.nl</email>
</deliveryAddress>
<articles>
<article>
<quantity>2</quantity>
<articleNumber>9700000</articleNumber>
<articleDescription>(Entreebord openingstijden A4 oblong) week 49 - nl-NL - entreebord A4 oblong_openingstijden 2025</articleDescription>
<ignoreMax>true</ignoreMax>
</article>
</articles>
</order>
Location: Same as input file
Naming: processing_report_YYYYMMDD_HHMMSS.txt
Purpose: Detailed processing log and audit trail
Sections:
Example Output:
================================================================================
OPENING HOURS PROCESSING REPORT
================================================================================
SUMMARY
--------------------------------------------------------------------------------
Status: SUCCESS
Input File: /data/stores.json
Output Directory: /data
Start Time: 2025-12-09 15:30:45
End Time: 2025-12-09 15:30:46
Processing Time: 27ms
STATISTICS
--------------------------------------------------------------------------------
Total Stores Processed: 21
ISO Groups Processed: 7
CSV Files Generated: 7
XML Files Generated: 21
Warnings: 2
Errors Encountered: 0
...
| Country | ISO Code | Time Format | Example | Closed Text |
|---|---|---|---|---|
| Netherlands | nl-NL | 08:00 - 18:00 | Colon, leading zeros | Gesloten |
| France | fr-FR | 8h00 - 18h00 | h separator, no leading zero | Fermé |
| Germany | de-DE | 08.00 - 18.00 | Dot separator, leading zeros | Geschlossen |
| Austria | de-AT | 08.00 - 18.00 | Dot separator, leading zeros | Geschlossen |
| Poland | pl-PL | 08:00 - 18:00 | Colon, leading zeros | Zamknięte |
| Slovakia | sk-SK | 08:00 - 18:00 | Colon, leading zeros | Zatvorené |
| Italy | it-IT | 08:00 - 18:00 | Colon, leading zeros | Chiuso |
| Spain | es-ES | 08:00 - 18:00 | Colon, leading zeros | Cerrado |
Adding New Countries: See CODE_DOCUMENTATION.md → Extensibility
❌ These will cause validation errors if empty or missing:
store - Store numberstreet - Street addresspostal_code - Postal codecity - City namelanguage - Language codecountry - Country codeweek - Week numberyear - Year✅ These are allowed to be empty:
name_sm - Store manager namemonday through sunday - Opening hours (empty = closed)number_of_signs - Defaults to 2 if empty⚠️ At least ONE day must have opening hours
⚠️ Address parsing supports letter suffixes
| Type | Description | Action |
|---|---|---|
| FILE_NOT_FOUND | Input file doesn't exist | Stop processing |
| VALIDATION_ERROR | Field validation failed | Skip store, continue |
| INVALID_JSON_STRUCTURE | JSON format incorrect | Stop processing |
| MISSING_COUNTRY_RULES | Unknown ISO code | Skip group, continue |
| CSV_GENERATION_ERROR | CSV creation failed | Log error, continue |
| XML_GENERATION_ERROR | XML creation failed | Log error, continue |
ERRORS
--------------------------------------------------------------------------------
Error #1:
Type: VALIDATION_ERROR
Time: 2025-12-09 15:30:45
Message: Failed to parse store item at index 3
Store: 1042
Details:
FormatException: Field Street (street) cannot be empty
Store data: {store: 1042, street: , postal_code: 1234 AB, ...}
Input (single_store.json):
[
{
"store": "1001",
"name_sm": "Jane Smith",
"street": "Main Street",
"house_no": "123",
"house_no_suffix": "",
"postal_code": "1234 AB",
"city": "Amsterdam",
"language": "NL",
"country": "NL",
"monday": "08:00 - 18:00",
"tuesday": "08:00 - 18:00",
"wednesday": "08:00 - 18:00",
"thursday": "08:00 - 18:00",
"friday": "08:00 - 18:00",
"saturday": "08:00 - 17:00",
"sunday": "",
"number_of_signs": 2,
"week": 49,
"year": 2025
}
]
Command:
dart run bin/dart_action_openingstijdenbordjes_convert_to_csv_and_xml.dart -i single_store.json
Output:
csv_output/Week 49 - nl-NL - A4-poster openingstijden 2025.csvxml_output/1001_Openingstijden week 49.xmlprocessing_report_20251209_153045.txtInput: 21 stores across 7 countries
Output:
Console Output:
════════════════════════════════════════════════════════════
PROCESSING SUMMARY
════════════════════════════════════════════════════════════
Status: SUCCESS
Stores Processed: 21
CSV Files: 7
XML Files: 21
Processing Time: 27ms
════════════════════════════════════════════════════════════
Input: Contains stores with empty required fields
Output:
WARNING: Error parsing store item at index 2 (store: 1003):
FormatException: Field Street (street) cannot be empty
════════════════════════════════════════════════════════════
PROCESSING SUMMARY
════════════════════════════════════════════════════════════
Status: COMPLETED WITH ERRORS
Stores Processed: 20
CSV Files: 7
XML Files: 20
Errors: 1
════════════════════════════════════════════════════════════
| Document | Description |
|---|---|
README.md |
This file - user guide and overview |
CODE_DOCUMENTATION.md |
Complete code documentation with architecture |
test/README.md |
Testing guide and test documentation |
VALIDATION_IMPLEMENTATION.md |
Detailed validation rules |
ADDRESS_PARSER_FIX.md |
Address parsing details |
TEST_FIXES.md |
Test implementation fixes |
dart test
# Model tests
dart test test/models/store_model_test.dart
# Utility tests
dart test test/utils/time_formatter_test.dart
dart test test/utils/address_parser_test.dart
dart test test/utils/date_utils_test.dart
# Service tests
dart test test/services/report_generator_test.dart
Cause: File path is incorrect or file doesn't exist
Solution:
# Use absolute paths
dart run bin/dart_action_openingstijdenbordjes_convert_to_csv_and_xml.dart -i /absolute/path/to/file.json
# Or relative from current directory
dart run bin/dart_action_openingstijdenbordjes_convert_to_csv_and_xml.dart -i ./relative/path/to/file.json
Cause: Required field has no value
Solution: Add value to the field in your JSON:
{
"store": "1001", // ✅ Has value
"street": "", // ❌ Empty - will fail
...
}
Cause: All 7 days are empty
Solution: Add hours for at least one day:
{
"monday": "08:00 - 18:00", // ✅ At least one day
"tuesday": "",
...
"sunday": "",
...
}
Cause: Country/language combination not supported
Solution: Add rules to lib/config/country_config.dart or contact support
# View full report
cat /path/to/processing_report_*.txt
# View only errors
grep -A 20 "ERRORS" /path/to/processing_report_*.txt
# View only warnings
grep -A 20 "WARNINGS" /path/to/processing_report_*.txt
Typical Performance:
Memory Usage:
Optimization Tips:
See LICENSE file for details.
Issues: Create an issue in the repository
Documentation: See CODE_DOCUMENTATION.md
Tests: Run dart test to verify installation
Current Version: 0.0.4
Changelog:
# 1. Install Dart
brew install dart
# 2. Get dependencies
dart pub get
# 3. Run with your data
dart run bin/dart_action_openingstijdenbordjes_convert_to_csv_and_xml.dart -i /path/to/your/data.json
# 4. Check outputs
ls -la csv_output/
ls -la xml_output/
cat processing_report_*.txt
Done! Your CSV and XML files are ready to use. 🎉