| 17 | 35 | return entry |
| 18 | 36 | |
| 19 | 37 | |
| 38 | def _format_date_us_style(date) -> str: |
| 39 | """Format date in US style: MM/DD/YYYY""" |
| 40 | return f"{date.month:02d}/{date.day:02d}/{date.year:04d}" |
| 41 | |
| 42 | |
| 43 | def _format_currency_us_style_usd(change: int) -> str: |
| 44 | """Format currency in US style for USD: $12.34 or ($12.34) for negative.""" |
| 45 | dollars = abs(change) // 100 |
| 46 | cents = abs(change) % 100 |
| 47 | |
| 48 | # Format dollars with comma separators |
| 49 | dollar_str = f"{dollars:,}" |
| 50 | |
| 51 | # Build the final string |
| 52 | if change < 0: |
| 53 | result = f"(${dollar_str}.{cents:02d})" |
| 54 | else: |
| 55 | result = f"${dollar_str}.{cents:02d} " |
| 56 | |
| 57 | return result.rjust(13) |
| 58 | |
| 59 | |
| 60 | def _format_currency_us_style_eur(change: int) -> str: |
| 61 | """Format currency in US style for EUR: €12.34 or (€12.34) for negative.""" |
| 62 | euros = abs(change) // 100 |
| 63 | cents = abs(change) % 100 |
| 64 | |
| 65 | # Format euros with comma separators |
| 66 | euro_str = f"{euros:,}" |
| 67 | |
| 68 | # Build the final string |
| 69 | if change < 0: |
| 70 | result = f"(€{euro_str}.{cents:02d})" |
| 71 | else: |
| 72 | result = f"€{euro_str}.{cents:02d} " |
| 73 | |
| 74 | return result.rjust(13) |
| 75 | |
| 76 | |
| 77 | def _format_currency_nl_style_usd(change: int) -> str: |
| 78 | """Format currency in Dutch style for USD: $ 12,34 or $ -12,34 for negative.""" |
| 79 | dollars = abs(change) // 100 |
| 80 | cents = abs(change) % 100 |
| 81 | |
| 82 | # Format dollars with period separators (Dutch style) |
| 83 | dollar_str = f"{dollars:,}".replace(',', '.') |
| 84 | |
| 85 | # Build the final string |
| 86 | if change < 0: |
| 87 | result = f"$ -{dollar_str},{cents:02d} " |
| 88 | else: |
| 89 | result = f"$ {dollar_str},{cents:02d} " |
| 90 | |
| 91 | return result.rjust(13) |
| 92 | |
| 93 | |
| 94 | def _generate_header_us_style() -> str: |
| 95 | """Generate header row for US style table.""" |
| 96 | table = 'Date' |
| 97 | for _ in range(7): |
| 98 | table += ' ' |
| 99 | table += '| Description' |
| 100 | for _ in range(15): |
| 101 | table += ' ' |
| 102 | table += '| Change' |
| 103 | for _ in range(7): |
| 104 | table += ' ' |
| 105 | return table |
| 106 | |
| 107 | |
| 108 | def _generate_header_nl_style() -> str: |
| 109 | """Generate header row for Dutch style table.""" |
| 110 | table = 'Datum' |
| 111 | for _ in range(6): |
| 112 | table += ' ' |
| 113 | table += '| Omschrijving' |
| 114 | for _ in range(14): |
| 115 | table += ' ' |
| 116 | table += '| Verandering' |
| 117 | for _ in range(2): |
| 118 | table += ' ' |
| 119 | return table |
| 120 | |
| 121 | |
| 122 | def _format_currency_nl_style_eur(change: int) -> str: |
| 123 | """Format currency in Dutch style for EUR: € 12,34 or € -12,34 for negative.""" |
| 124 | euros = abs(change) // 100 |
| 125 | cents = abs(change) % 100 |
| 126 | |
| 127 | # Format euros with period separators (Dutch style) |
| 128 | euro_str = f"{euros:,}".replace(',', '.') |
| 129 | |
| 130 | # Build the final string |
| 131 | if change < 0: |
| 132 | result = f"€ -{euro_str},{cents:02d} " |
| 133 | else: |
| 134 | result = f"€ {euro_str},{cents:02d} " |
| 135 | |
| 136 | return result.rjust(13) |
| 137 | |
| 138 | |
| 139 | def _format_description(description: str) -> str: |
| 140 | """Format description, truncating if necessary to 25 characters.""" |
| 141 | if len(description) > 25: |
| 142 | return description[:22] + '...' |
| 143 | else: |
| 144 | return description.ljust(25) |
| 145 | |
| 146 | |
| 147 | def _format_date_nl_style(date) -> str: |
| 148 | """Format date in Dutch style: DD-MM-YYYY""" |
| 149 | return f"{date.day:02d}-{date.month:02d}-{date.year:04d}" |
| 150 | |
| 151 | |
| 152 | def _sort_entries_by_date_change_description(entries: list[LedgerEntry]) -> list[LedgerEntry]: |
| 153 | """Sort entries by date, then by change amount, then by description.""" |
| 154 | return sorted(entries, key=lambda entry: (entry.date, entry.change, entry.description)) |
| 155 | |
| 156 | |
| 157 | def _format_entries_us_style(currency: str, entries: list[LedgerEntry]) -> str: |
| 158 | """Format ledger entries in US style.""" |
| 159 | table = _generate_header_us_style() |
| 160 | sorted_entries = _sort_entries_by_date_change_description(entries) |
| 161 | |
| 162 | for entry in sorted_entries: |
| 163 | table += '\n' |
| 164 | |
| 165 | # Write entry date to table |
| 166 | date_str = _format_date_us_style(entry.date) |
| 167 | table += date_str |
| 168 | table += ' | ' |
| 169 | |
| 170 | # Write entry description to table |
| 171 | formatted_desc = _format_description(entry.description) |
| 172 | table += formatted_desc |
| 173 | table += ' | ' |
| 174 | |
| 175 | # Write entry change to table |
| 176 | if currency == 'USD': |
| 177 | change_str = _format_currency_us_style_usd(entry.change) |
| 178 | table += change_str |
| 179 | elif currency == 'EUR': |
| 180 | change_str = _format_currency_us_style_eur(entry.change) |
| 181 | table += change_str |
| 182 | |
| 183 | return table |
| 184 | |
| 185 | |
| 186 | def _format_entries_nl_style(currency: str, entries: list[LedgerEntry]) -> str: |
| 187 | """Format ledger entries in Dutch style.""" |
| 188 | table = _generate_header_nl_style() |
| 189 | sorted_entries = _sort_entries_by_date_change_description(entries) |
| 190 | |
| 191 | for entry in sorted_entries: |
| 192 | table += '\n' |
| 193 | |
| 194 | # Write entry date to table |
| 195 | date_str = _format_date_nl_style(entry.date) |
| 196 | table += date_str |
| 197 | table += ' | ' |
| 198 | |
| 199 | # Write entry description to table |
| 200 | formatted_desc = _format_description(entry.description) |
| 201 | table += formatted_desc |
| 202 | table += ' | ' |
| 203 | |
| 204 | # Write entry change to table |
| 205 | if currency == 'USD': |
| 206 | change_str = _format_currency_nl_style_usd(entry.change) |
| 207 | table += change_str |
| 208 | elif currency == 'EUR': |
| 209 | change_str = _format_currency_nl_style_eur(entry.change) |
| 210 | table += change_str |
| 211 | |
| 212 | return table |
| 213 | |
| 214 | |
| 20 | 215 | def format_entries(currency: str, locale: str, entries: list[LedgerEntry]) -> str: |
| 21 | 216 | if locale == 'en_US': |
| 22 | | # Generate Header Row |
| 23 | | table = 'Date' |
| 24 | | for _ in range(7): |
| 25 | | table += ' ' |
| 26 | | table += '| Description' |
| 27 | | for _ in range(15): |
| 28 | | table += ' ' |
| 29 | | table += '| Change' |
| 30 | | for _ in range(7): |
| 31 | | table += ' ' |
| 32 | | |
| 33 | | while len(entries) > 0: |
| 34 | | table += '\n' |
| 35 | | |
| 36 | | # Find next entry in order |
| 37 | | min_entry_index = -1 |
| 38 | | for i in range(len(entries)): |
| 39 | | entry = entries[i] |
| 40 | | if min_entry_index < 0: |
| 41 | | min_entry_index = i |
| 42 | | continue |
| 43 | | min_entry = entries[min_entry_index] |
| 44 | | if entry.date < min_entry.date: |
| 45 | | min_entry_index = i |
| 46 | | continue |
| 47 | | if ( |
| 48 | | entry.date == min_entry.date and |
| 49 | | entry.change < min_entry.change |
| 50 | | ): |
| 51 | | min_entry_index = i |
| 52 | | continue |
| 53 | | if ( |
| 54 | | entry.date == min_entry.date and |
| 55 | | entry.change == min_entry.change and |
| 56 | | entry.description < min_entry.description |
| 57 | | ): |
| 58 | | min_entry_index = i |
| 59 | | continue |
| 60 | | entry = entries[min_entry_index] |
| 61 | | entries.pop(min_entry_index) |
| 62 | | |
| 63 | | # Write entry date to table |
| 64 | | month = entry.date.month |
| 65 | | month = str(month) |
| 66 | | if len(month) < 2: |
| 67 | | month = '0' + month |
| 68 | | date_str = month |
| 69 | | date_str += '/' |
| 70 | | day = entry.date.day |
| 71 | | day = str(day) |
| 72 | | if len(day) < 2: |
| 73 | | day = '0' + day |
| 74 | | date_str += day |
| 75 | | date_str += '/' |
| 76 | | year = entry.date.year |
| 77 | | year = str(year) |
| 78 | | while len(year) < 4: |
| 79 | | year = '0' + year |
| 80 | | date_str += year |
| 81 | | table += date_str |
| 82 | | table += ' | ' |
| 83 | | |
| 84 | | # Write entry description to table |
| 85 | | # Truncate if necessary |
| 86 | | if len(entry.description) > 25: |
| 87 | | for i in range(22): |
| 88 | | table += entry.description[i] |
| 89 | | table += '...' |
| 90 | | else: |
| 91 | | for i in range(25): |
| 92 | | if len(entry.description) > i: |
| 93 | | table += entry.description[i] |
| 94 | | else: |
| 95 | | table += ' ' |
| 96 | | table += ' | ' |
| 97 | | |
| 98 | | # Write entry change to table |
| 99 | | if currency == 'USD': |
| 100 | | change_str = '' |
| 101 | | if entry.change < 0: |
| 102 | | change_str = '(' |
| 103 | | change_str += '$' |
| 104 | | change_dollar = abs(int(entry.change / 100.0)) |
| 105 | | dollar_parts = [] |
| 106 | | while change_dollar > 0: |
| 107 | | dollar_parts.insert(0, str(change_dollar % 1000)) |
| 108 | | change_dollar = change_dollar // 1000 |
| 109 | | if len(dollar_parts) == 0: |
| 110 | | change_str += '0' |
| 111 | | else: |
| 112 | | while True: |
| 113 | | change_str += dollar_parts[0] |
| 114 | | dollar_parts.pop(0) |
| 115 | | if len(dollar_parts) == 0: |
| 116 | | break |
| 117 | | change_str += ',' |
| 118 | | change_str += '.' |
| 119 | | change_cents = abs(entry.change) % 100 |
| 120 | | change_cents = str(change_cents) |
| 121 | | if len(change_cents) < 2: |
| 122 | | change_cents = '0' + change_cents |
| 123 | | change_str += change_cents |
| 124 | | if entry.change < 0: |
| 125 | | change_str += ')' |
| 126 | | else: |
| 127 | | change_str += ' ' |
| 128 | | while len(change_str) < 13: |
| 129 | | change_str = ' ' + change_str |
| 130 | | table += change_str |
| 131 | | elif currency == 'EUR': |
| 132 | | change_str = '' |
| 133 | | if entry.change < 0: |
| 134 | | change_str = '(' |
| 135 | | change_str += u'€' |
| 136 | | change_euro = abs(int(entry.change / 100.0)) |
| 137 | | euro_parts = [] |
| 138 | | while change_euro > 0: |
| 139 | | euro_parts.insert(0, str(change_euro % 1000)) |
| 140 | | change_euro = change_euro // 1000 |
| 141 | | if len(euro_parts) == 0: |
| 142 | | change_str += '0' |
| 143 | | else: |
| 144 | | while True: |
| 145 | | change_str += euro_parts[0] |
| 146 | | euro_parts.pop(0) |
| 147 | | if len(euro_parts) == 0: |
| 148 | | break |
| 149 | | change_str += ',' |
| 150 | | change_str += '.' |
| 151 | | change_cents = abs(entry.change) % 100 |
| 152 | | change_cents = str(change_cents) |
| 153 | | if len(change_cents) < 2: |
| 154 | | change_cents = '0' + change_cents |
| 155 | | change_str += change_cents |
| 156 | | if entry.change < 0: |
| 157 | | change_str += ')' |
| 158 | | else: |
| 159 | | change_str += ' ' |
| 160 | | while len(change_str) < 13: |
| 161 | | change_str = ' ' + change_str |
| 162 | | table += change_str |
| 163 | | return table |
| 217 | return _format_entries_us_style(currency, entries) |
| 164 | 218 | elif locale == 'nl_NL': |
| 165 | | # Generate Header Row |
| 166 | | table = 'Datum' |
| 167 | | for _ in range(6): |
| 168 | | table += ' ' |
| 169 | | table += '| Omschrijving' |
| 170 | | for _ in range(14): |
| 171 | | table += ' ' |
| 172 | | table += '| Verandering' |
| 173 | | for _ in range(2): |
| 174 | | table += ' ' |
| 175 | | |
| 176 | | while len(entries) > 0: |
| 177 | | table += '\n' |
| 178 | | |
| 179 | | # Find next entry in order |
| 180 | | min_entry_index = -1 |
| 181 | | for i in range(len(entries)): |
| 182 | | entry = entries[i] |
| 183 | | if min_entry_index < 0: |
| 184 | | min_entry_index = i |
| 185 | | continue |
| 186 | | min_entry = entries[min_entry_index] |
| 187 | | if entry.date < min_entry.date: |
| 188 | | min_entry_index = i |
| 189 | | continue |
| 190 | | if ( |
| 191 | | entry.date == min_entry.date and |
| 192 | | entry.change < min_entry.change |
| 193 | | ): |
| 194 | | min_entry_index = i |
| 195 | | continue |
| 196 | | if ( |
| 197 | | entry.date == min_entry.date and |
| 198 | | entry.change == min_entry.change and |
| 199 | | entry.description < min_entry.description |
| 200 | | ): |
| 201 | | min_entry_index = i |
| 202 | | continue |
| 203 | | entry = entries[min_entry_index] |
| 204 | | entries.pop(min_entry_index) |
| 205 | | |
| 206 | | # Write entry date to table |
| 207 | | day = entry.date.day |
| 208 | | day = str(day) |
| 209 | | if len(day) < 2: |
| 210 | | day = '0' + day |
| 211 | | date_str = day |
| 212 | | date_str += '-' |
| 213 | | month = entry.date.month |
| 214 | | month = str(month) |
| 215 | | if len(month) < 2: |
| 216 | | month = '0' + month |
| 217 | | date_str += month |
| 218 | | date_str += '-' |
| 219 | | year = entry.date.year |
| 220 | | year = str(year) |
| 221 | | while len(year) < 4: |
| 222 | | year = '0' + year |
| 223 | | date_str += year |
| 224 | | table += date_str |
| 225 | | table += ' | ' |
| 226 | | |
| 227 | | # Write entry description to table |
| 228 | | # Truncate if necessary |
| 229 | | if len(entry.description) > 25: |
| 230 | | for i in range(22): |
| 231 | | table += entry.description[i] |
| 232 | | table += '...' |
| 233 | | else: |
| 234 | | for i in range(25): |
| 235 | | if len(entry.description) > i: |
| 236 | | table += entry.description[i] |
| 237 | | else: |
| 238 | | table += ' ' |
| 239 | | table += ' | ' |
| 240 | | |
| 241 | | # Write entry change to table |
| 242 | | if currency == 'USD': |
| 243 | | change_str = '$ ' |
| 244 | | if entry.change < 0: |
| 245 | | change_str += '-' |
| 246 | | change_dollar = abs(int(entry.change / 100.0)) |
| 247 | | dollar_parts = [] |
| 248 | | while change_dollar > 0: |
| 249 | | dollar_parts.insert(0, str(change_dollar % 1000)) |
| 250 | | change_dollar = change_dollar // 1000 |
| 251 | | if len(dollar_parts) == 0: |
| 252 | | change_str += '0' |
| 253 | | else: |
| 254 | | while True: |
| 255 | | change_str += dollar_parts[0] |
| 256 | | dollar_parts.pop(0) |
| 257 | | if len(dollar_parts) == 0: |
| 258 | | break |
| 259 | | change_str += '.' |
| 260 | | change_str += ',' |
| 261 | | change_cents = abs(entry.change) % 100 |
| 262 | | change_cents = str(change_cents) |
| 263 | | if len(change_cents) < 2: |
| 264 | | change_cents = '0' + change_cents |
| 265 | | change_str += change_cents |
| 266 | | change_str += ' ' |
| 267 | | while len(change_str) < 13: |
| 268 | | change_str = ' ' + change_str |
| 269 | | table += change_str |
| 270 | | elif currency == 'EUR': |
| 271 | | change_str = u'€ ' |
| 272 | | if entry.change < 0: |
| 273 | | change_str += '-' |
| 274 | | change_euro = abs(int(entry.change / 100.0)) |
| 275 | | euro_parts = [] |
| 276 | | while change_euro > 0: |
| 277 | | euro_parts.insert(0, str(change_euro % 1000)) |
| 278 | | change_euro = change_euro // 1000 |
| 279 | | if len(euro_parts) == 0: |
| 280 | | change_str += '0' |
| 281 | | else: |
| 282 | | while True: |
| 283 | | change_str += euro_parts[0] |
| 284 | | euro_parts.pop(0) |
| 285 | | if len(euro_parts) == 0: |
| 286 | | break |
| 287 | | change_str += '.' |
| 288 | | change_str += ',' |
| 289 | | change_cents = abs(entry.change) % 100 |
| 290 | | change_cents = str(change_cents) |
| 291 | | if len(change_cents) < 2: |
| 292 | | change_cents = '0' + change_cents |
| 293 | | change_str += change_cents |
| 294 | | change_str += ' ' |
| 295 | | while len(change_str) < 13: |
| 296 | | change_str = ' ' + change_str |
| 297 | | table += change_str |
| 298 | | return table |
| 219 | return _format_entries_nl_style(currency, entries) |
| 220 | else: |
| 221 | raise ValueError(f"Unsupported locale: {locale}") |
| 299 | 222 | |