Monatsvorhersage-Fließtext überarbeiten: Struktur, Grammatik, viertes Textset
Textbaustein-System für die Monatsübersicht deutlich robuster und
natürlicher gemacht:
- Zwischenüberschriften ("Der Mond im ...", "Die Planeten im ...") durch
strukturierte Absatz-Rückgabe statt reinem String; Live-Seite und
HTML-/Joomla-Export rendern jetzt korrekt h3 -> h4 -> p.
- Deterministische statt zufällige Textbaustein-Auswahl (seeded statt
random_int), damit derselbe Monat/Jahr bei erneutem Aufruf denselben
Text liefert.
- Übergangsphrasen zwischen Ereignissätzen ("Anschließend", "Kurz
darauf" etc.) berücksichtigen jetzt den tatsächlichen Tagesabstand
und wiederholen sich nie zwei Mal hintereinander; Ereignisse am
selben Tag werden zu einem Satz zusammengefasst statt das Datum
doppelt zu nennen.
- Bugfix: Merkur-Elongationen und Venus-Konjunktionen landeten wegen
eines Typo im Typvergleich im generischen Fallback-Satz.
- Venus korrekt feminin dekliniert (sie/ihre) statt durchgängig "er".
- Morgen-/Abenddämmerung wird bei Merkur-Sichtbarkeit und Venus-
Helligkeit jetzt benannt (Venus-Sichtbarkeitsrichtung neu in
api.py berechnet).
- Meteorstrom- und Bedeckungs-Label für Fließtext aufbereitet statt
Rohdaten-Format ("Maximum X - CODE - ZHR ca. Y", "- Größte
Bedeckung") direkt auszugeben.
- Aufhänger-Satz: Priorität um Mondbedeckungen erweitert, Planeten-
parade entfernt; nur im Export fett hervorgehoben.
- Viertes Sky-Intro-Textset (V4) ergänzt, Rotation von year%3 auf
year%4 umgestellt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+479
-89
@@ -269,7 +269,7 @@ $seasonChanges = [];
|
||||
$timeChanges = [];
|
||||
$monthEventList = [];
|
||||
$starDataError = null;
|
||||
$monthNarrativeText = '';
|
||||
$monthNarrativeText = [];
|
||||
$observationTipInsertSql = '';
|
||||
$observationTipCalendarIcs = '';
|
||||
$observationTipInsertPayloadJson = '[]';
|
||||
@@ -761,7 +761,15 @@ if (!function_exists('buildObservationTipCalendarIcs')) {
|
||||
}
|
||||
|
||||
if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
function monthForecastBuildNarrativeText(array $monthEventList, int $monthNumber, string $monthName, int $year): string
|
||||
/**
|
||||
* @return list<array{heading: ?string, text: string, emphasize?: bool}> Abschnitte des
|
||||
* Monatstexts; `heading` ist gesetzt, wenn der Abschnitt eine eigene
|
||||
* Zwischenueberschrift beginnt (z. B. "Der Mond im September 2026"), sonst null.
|
||||
* `emphasize` markiert den kurzen Aufhaenger-Satz am Anfang (falls vorhanden) -
|
||||
* wird auf der Seite selbst nicht hervorgehoben, nur beim HTML-/Joomla-Export
|
||||
* als <strong> ausgegeben.
|
||||
*/
|
||||
function monthForecastBuildNarrativeText(array $monthEventList, int $monthNumber, string $monthName, int $year): array
|
||||
{
|
||||
$filteredEvents = array_values(array_filter(
|
||||
$monthEventList,
|
||||
@@ -814,20 +822,47 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
12 => "Lange Nächte und ein glanzvoller Winterhimmel – das ist der Dezember. Die Sommersternbilder ziehen sich in den frühen Abendstunden zurück; Orion, Stier, Zwillinge und Fuhrmann haben längst ihren Platz eingenommen. Sirius erscheint früh über dem Südosthorizont. In der Neujahrsnacht erreicht er gegen Mitternacht seinen Tageshöchstpunkt und steht dann von Deutschland aus fast genau im Süden.\n\nIm Südwesten verbleiben Reste des Herbsthimmels: Wassermann, Fische und Walfisch unterhalb des Herbstvierecks, lichtschwach und weitläufig. Zwischen Walfisch und Orion windet sich der Eridanus – seine Sterne sind unauffällig, doch sein geschwungener Verlauf lädt bei klarer Nacht zu einer kleinen Himmelsreise ein.",
|
||||
];
|
||||
|
||||
$skyIntroByMonth = match ($year % 3) {
|
||||
$skyIntroByMonthV4 = [
|
||||
1 => "Hoch im Süden dominiert im Januar eine Sternfigur, die selbst Laien sofort wiedererkennen: der Orion mit seinen sieben hellen Hauptsternen. Die drei Gürtelsterne in der Mitte sind sein Markenzeichen; direkt darunter lässt sich schon im Fernglas der neblige Schleier des Orionnebels aufspüren. Die rechte untere Ecke der Figur markiert Rigel, einer der hellsten Sterne des gesamten Himmels.\n\nRigel ist gleichzeitig Eckpunkt einer noch größeren Figur: des Wintersechsecks. Im Uhrzeigersinn reihen sich daran Sirius im Großen Hund, Prokyon im Kleinen Hund, Castor in den Zwillingen, Capella im Fuhrmann und Aldebaran im Stier. Durch genau diesen Bereich zieht sich auch die Wintermilchstraße, von Großem Hund über Einhorn, Zwillinge, Stier, Fuhrmann und Perseus bis zur Kassiopeia – nur ist sie deutlich blasser als ihr Sommerpendant, weil unser Blick jetzt eher zum Rand der Galaxis geht.",
|
||||
2 => "Selten zeigt sich der Winterhimmel so sternenreich wie an einem klaren Februarabend. Um 22 Uhr, auf diese Zeit bezieht sich die Sternkarte, haben die Sternbilder rund um den Orion den Meridian bereits überquert und ziehen langsam nach Westen. Unter dem Orion funkelt Sirius, der hellste Fixstern des Nachthimmels überhaupt. Rechts oben leuchtet der Stier mit Aldebaran, darüber der Fuhrmann mit der gelblichen Capella, links die Zwillinge mit Castor und Pollux; unterhalb von ihnen sitzt Prokyon, der Hauptstern des Kleinen Hundes.\n\nIm Osten kündigt sich derweil schon der Frühling an: Der Löwe mit Regulus ist komplett aufgegangen, und tief darunter blitzt Alphard als einsamer heller Punkt in der Wasserschlange. Bis die Jungfrau vollständig über dem Horizont steht, dauert es dagegen noch bis nach Mitternacht.",
|
||||
3 => "Im März treffen zwei Jahreszeiten am Himmel aufeinander. Am frühen Abend sind die Wintersternbilder noch da – Orion, Großer Hund, Kleiner Hund, Zwillinge, Fuhrmann, Stier –, doch im Lauf der Nacht wandern sie unaufhaltsam Richtung Westen, während im Osten schon die Frühlingssternbilder nachrücken. Zur Mitternachtsstunde gehört der Himmel bereits den Sternbildern rund um den Löwen.\n\nZwischen den Zwillingen und dem Löwen duckt sich der unscheinbare Krebs – wer ihn unter dunklem Himmel aufsucht, wird mit dem offenen Sternhaufen Praesepe belohnt, liebevoll „Krippe\" genannt und als milchiger Fleck erkennbar. Fast im Zenit steht jetzt der Große Wagen, so hoch wie kaum sonst im Jahr. Im Südosten geht Spica auf, im Nordosten leuchtet der orangefarbene Arktur. Tief am Südhimmel reckt sich die Wasserschlange empor, meist nur zu erahnen am einsamen Alphard, dessen Name „der Einsame\" bezeichnenderweise passt. Und ganz knapp über dem Horizont wartet noch der kompakte Rabe.",
|
||||
4 => "Drei Sterne prägen den Aprilhimmel wie ein Wegweiser: Regulus im Löwen, Spica in der Jungfrau und der orangefarbene Arktur im Bärenhüter – gemeinsam bilden sie das Frühlingsdreieck. Mittendrin steht der Löwe hoch im Süden, dessen Sternfigur bei etwas Fantasie tatsächlich an eine liegende Raubkatze erinnert.\n\nFast senkrecht über uns steht jetzt der Große Wagen. Zwischen ihm und dem Löwen liegen nur unscheinbare Sternbilder – Kleiner Löwe, Jagdhunde, Haar der Berenike –, ebenso sternarm wie der Streifen zwischen Löwe und Horizont, wo sich die langgestreckte Wasserschlange entlangzieht. Etwas leichter zu finden sind dagegen Becher und Rabe, deren kompakte Formen trotz mäßiger Helligkeit auffallen.",
|
||||
5 => "Wenn ein Stern den Maiabend beherrscht, dann Arktur: orange, hell und hoch im Süden, als Hauptstern des Bärenhüters kaum zu übersehen. Tiefer im Süden schimmert Spica in der Jungfrau, im Südwesten hält Regulus im Löwen die Stellung – zusammen spannen die drei Sterne das Frühlingsdreieck auf. Etwas blasser liegt darüber das Sternbild Haar der Berenike.\n\nAm Südosthorizont deutet sich mit dem tief stehenden Skorpion und seinem Hauptstern Antares bereits der Sommer an. Im Westen sinken die letzten Wintersterne, während im Osten schon Wega in der Leier aufsteigt. Zwischen Wega und Arktur liegen das eher blasse Herkules und der elegante Halbkreis der Nördlichen Krone.",
|
||||
6 => "Der Skorpion bleibt im Juni ein Horizontgast: Sein rötlicher Hauptstern Antares zeigt sich zwar tief im Südosten, steigt von unseren Breiten aus aber nie besonders hoch. Weit darüber thront der Große Wagen, das auffälligste Stück des Großen Bären – seine geschwungene Deichsel weist zuverlässig erst zu Arktur, dann weiter zu Spica. Im Südwesten neigt sich der Löwe bereits dem Untergang zu.\n\nGleich links über Arktur öffnet sich ein zarter Halbkreis: die Nördliche Krone, deren hellster Stern Gemma zu Recht „Edelstein\" heißt. Darunter breiten sich Schlangenträger und die zweigeteilte Schlange aus – lichtschwach, aber unter dunklem Himmel gut zu verfolgen. Den eigentlichen Höhepunkt des Juniabends liefert jedoch der Osten: Dort erhebt sich das Sommerdreieck aus Wega in der Leier, Deneb im Schwan und Altair im Adler – ein Trio, das den Himmel die kommenden Monate begleiten wird.",
|
||||
7 => "Wirklich hoch steigen Skorpion und Schütze im Juli nur für Beobachter im Süden Europas – von Deutschland aus bleiben beide Tierkreissternbilder flach über dem Südhorizont hängen. Darüber reihen sich Schlangenträger, Schlange und Herkules, allesamt eher unauffällig, aber typisch für den sommerlichen Sternhimmel.\n\nDeutlich mehr Aufmerksamkeit verdienen Arktur, Wega, Deneb und Altair, die vier hellen Fixpunkte dieser Jahreszeit. Wega, Deneb und Altair formen dabei das Sommerdreieck. Zwar zieht sich auch die Milchstraße schon als Band über den Himmel, bleibt in den noch nicht ganz dunklen Julinächten aber oft blass – ihre beste Zeit kommt erst im August. Der Große Bär ist inzwischen weiter in den Nordwesten gewandert.",
|
||||
8 => "Kaum ein Monat lohnt sich für Sterngucker mehr als der August. Nach 22 Uhr wird es endlich wieder richtig dunkel, und ein Stern nach dem anderen erscheint am Himmel. Um 23 Uhr hat sich das Sommerdreieck bereits im Zenit eingerichtet: Wega fast senkrecht über uns, Deneb im Schwan links davon, Altair im Adler deutlich tiefer – ein unverwechselbares Muster.\n\nMit den länger werdenden Dunkelphasen kehrt auch die Milchstraße zurück: Als schimmerndes Band zieht sie mitten durch das Sommerdreieck, am schönsten fernab jeder Lichtquelle. Im Osten deuten Pegasus und Andromeda schon den nahenden Herbst an. Und um den 12. August erreicht der Meteorstrom der Perseiden sein Maximum – Staubkörner des Kometen Swift-Tuttle verglühen dann zahlreich als Sternschnuppen in der Atmosphäre.",
|
||||
9 => "Der September bringt die Dunkelheit zurück, und mit ihr wieder richtige Sternennächte. Gegen 21.30 Uhr ist der Himmel schon gut gefüllt, allen voran mit Wega, die hoch über uns als Erste ins Auge fällt. Wer genauer hinschaut, entdeckt um sie herum die kleine Raute der Leier. Zusammen mit Altair im Adler und Deneb im Schwan bildet Wega weiterhin das große Sommerdreieck, das jetzt schon bis in den Herbst hineinreicht.\n\nVersteckt im und am Rand des Sommerdreiecks liegen zwei kleine, aber einprägsame Sternbilder: Pfeil und Delfin – nicht hell, dafür durch ihre kompakte Form überraschend leicht zu finden. Auch die Milchstraße zeigt sich wieder deutlicher als fahles Band. Während im Westen die letzten Sommersternbilder verschwinden, tauchen im Osten die ersten Herbstgestalten auf – dazwischen liegt ein eher sternarmer Himmelsabschnitt.",
|
||||
10 => "Seit die Sonne Ende September den Himmelsäquator nach Süden überquert hat, sind die Nächte länger als die Tage – ein untrügliches Zeichen für den Herbst. Am frühen Oktoberabend hält sich noch das vertraute Sommerdreieck aus Wega, Deneb und Altair, doch im Lauf der Nacht übernehmen zunehmend die Herbststernbilder das Kommando.\n\nGegen 23 Uhr Sommerzeit steht das große Herbstviereck hoch am Himmel: drei seiner Ecken gehören zu Pegasus, die vierte zur Sternenkette der Andromeda. Darunter liegen die eher lichtschwachen Tierkreissternbilder Steinbock, Wassermann und Fische, die nur unter dunklem Himmel richtig zur Geltung kommen. Diphda im Walfisch gehört zu den wenigen helleren Sternen dieser Himmelsregion, und tief im Süden lässt sich zudem Fomalhaut im Südlichen Fisch erspähen.",
|
||||
11 => "Im November gehört die Himmelsbühne dem Herbstviereck – auch Pegasus-Quadrat genannt, mit drei Ecken aus dem Sternbild Pegasus und einer aus der Andromeda-Kette. Genau in der Andromeda lohnt sich ein genauerer Blick: Dort liegt die Andromedagalaxie, mit bloßem Auge das mit Abstand fernste sichtbare Objekt – unter dunklem Himmel als zarter, nebliger Fleck erkennbar.\n\nRundherum, im Südwesten und Süden, ziehen sich die eher blassen Sternbilder Wassermann, Walfisch und Fische entlang. Im Walfisch versteckt sich zudem Mira, einer der bekanntesten veränderlichen Sterne überhaupt. Im Osten dagegen kündigt sich bereits der Winter an: Die Kassiopeia zeigt ihr markantes W, Perseus steigt mit dem veränderlichen Algol empor, und im Stier fallen die Plejaden und der rötliche Aldebaran auf. Ganz am östlichen Horizont lässt sich schließlich schon Orion blicken.",
|
||||
12 => "Der Dezember zeigt den Winterhimmel in seiner vollen Pracht, und das schon früh am Abend. Die Sommersternbilder Schwan, Leier und Adler sind längst verschwunden; Orion, Stier, Zwillinge und Fuhrmann haben ihren festen Platz eingenommen. Auch Sirius, der hellste Stern des gesamten Nachthimmels, taucht schon früh über dem Südosthorizont auf. Ein besonderer Moment: In der Silvesternacht erreicht er gegen Mitternacht seinen höchsten Stand und steht dann von Deutschland aus fast exakt im Süden.\n\nIm Südwesten hält sich noch ein letzter Rest des Herbsthimmels – Wassermann, Fische und Walfisch, allesamt ausgedehnt, aber lichtschwach, unterhalb des Herbstvierecks. Zwischen Walfisch und Orion schlängelt sich außerdem der Eridanus: unauffällige Sterne, aber ein Verlauf, dem in einer klaren Nacht nachzuspüren sich lohnt.",
|
||||
];
|
||||
|
||||
// Echte Abwechslung entsteht ueber die Rotation der vier Varianten-Sets
|
||||
// (year % 4); pro Jahr gibt es genau einen Text je Monat.
|
||||
$skyIntroByMonth = match ($year % 4) {
|
||||
1 => $skyIntroByMonthV2,
|
||||
2 => $skyIntroByMonthV3,
|
||||
3 => $skyIntroByMonthV4,
|
||||
default => $skyIntroByMonthV1,
|
||||
};
|
||||
|
||||
foreach ($skyIntroByMonth as $introMonthNumber => $introText) {
|
||||
$skyIntroByMonth[$introMonthNumber] = [$introText];
|
||||
}
|
||||
|
||||
$paragraphs = [];
|
||||
$monthIntroVariants = $skyIntroByMonth[$monthNumber] ?? [($monthName . ' ' . $year . ' bietet einen abwechslungsreichen Himmel zwischen den jahreszeitlichen Sternbildern.')];
|
||||
$monthIntroIndex = (($year * 100) + $monthNumber) % count($monthIntroVariants);
|
||||
$paragraphs[] = $monthIntroVariants[$monthIntroIndex];
|
||||
// Fuegt einen Textabschnitt hinzu, optional mit eigener Zwischenueberschrift
|
||||
// (z. B. "Der Mond im September 2026"), die beim Rendern als <h4> erscheint.
|
||||
$addParagraph = static function (string $text, ?string $heading = null) use (&$paragraphs): void {
|
||||
if (trim($text) === '') {
|
||||
return;
|
||||
}
|
||||
$paragraphs[] = ['heading' => $heading, 'text' => $text];
|
||||
};
|
||||
|
||||
$addParagraph(
|
||||
$skyIntroByMonth[$monthNumber]
|
||||
?? ($monthName . ' ' . $year . ' bietet einen abwechslungsreichen Himmel zwischen den jahreszeitlichen Sternbildern.')
|
||||
);
|
||||
// Die Planeten-Ueberschrift soll nur einmal erscheinen, auch wenn Beobachtungs-
|
||||
// und Bewegungsereignisse als getrennte Absaetze ausgegeben werden.
|
||||
$planetsHeadingUsed = false;
|
||||
|
||||
$findEvents = static function (array $events, callable $filter, int $limit = 3): array {
|
||||
$matches = [];
|
||||
@@ -842,7 +877,88 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
return $matches;
|
||||
};
|
||||
|
||||
$describeEvents = static function (array $events): string {
|
||||
// Deterministisch statt zufaellig: gleiche Formulierung bei jedem Aufruf
|
||||
// fuer denselben Monat/Jahr (reproduzierbar bei erneutem Seitenaufruf),
|
||||
// aber Abwechslung von Monat zu Monat und Jahr zu Jahr.
|
||||
$seededPick = static function (array $options, string $seedKey, string $fallback = '') use ($year, $monthNumber): string {
|
||||
$options = array_values(array_filter($options, static fn ($option): bool => trim((string) $option) !== ''));
|
||||
if ($options === []) {
|
||||
return $fallback;
|
||||
}
|
||||
$hash = crc32($year . '|' . $monthNumber . '|' . $seedKey);
|
||||
return (string) $options[$hash % count($options)];
|
||||
};
|
||||
|
||||
// Natuerliche Aufzaehlung "A, B und C" statt "A sowie B sowie C".
|
||||
$naturalJoin = static function (array $parts, string $conjunction = 'und'): string {
|
||||
$parts = array_values(array_filter($parts, static fn ($part): bool => trim((string) $part) !== ''));
|
||||
if ($parts === []) {
|
||||
return '';
|
||||
}
|
||||
if (count($parts) === 1) {
|
||||
return $parts[0];
|
||||
}
|
||||
$last = array_pop($parts);
|
||||
return implode(', ', $parts) . ' ' . $conjunction . ' ' . $last;
|
||||
};
|
||||
|
||||
// Ermittelt den Tag im Monat aus "dd.mm." oder "dd.mm.yyyy", um den
|
||||
// zeitlichen Abstand zweier Ereignisse abzuschaetzen.
|
||||
$dayNumberOf = static function (string $dateStr): ?int {
|
||||
return preg_match('/^(\d{1,2})\./', trim($dateStr), $m) ? (int) $m[1] : null;
|
||||
};
|
||||
|
||||
// Variiert den Satzanfang ("Am {Datum} ..." bzw. "Ab dem {Datum} ...")
|
||||
// bei aufeinanderfolgenden Ereignissen, damit nicht jeder Satz eines
|
||||
// Absatzes gleich beginnt. index 0 bleibt immer unveraendert. Dieselbe
|
||||
// Uebergangsphrase wird nie zweimal direkt hintereinander verwendet, und
|
||||
// Phrasen wie "Nur wenige Tage spaeter" werden nur bei tatsaechlich
|
||||
// kurzem Abstand angeboten (sonst waeren sie schlicht falsch).
|
||||
$lastLeadInLabel = null;
|
||||
$varyLeadIn = static function (string $sentence, string $originalPrefix, string $datePhraseLower, int $index, string $seedKey, ?int $dayGap = null) use ($seededPick, &$lastLeadInLabel): string {
|
||||
if ($index === 0 || !str_starts_with($sentence, $originalPrefix)) {
|
||||
return $sentence;
|
||||
}
|
||||
$rest = substr($sentence, strlen($originalPrefix));
|
||||
$plain = $originalPrefix . $rest;
|
||||
|
||||
if ($dayGap !== null && $dayGap <= 4) {
|
||||
$variantMap = [
|
||||
'plain' => $plain,
|
||||
'nurWenigeTage' => 'Nur wenige Tage später, ' . $datePhraseLower . ', ' . $rest,
|
||||
'kurzDarauf' => 'Kurz darauf, ' . $datePhraseLower . ', ' . $rest,
|
||||
'wenigSpaeter' => 'Wenig später, ' . $datePhraseLower . ', ' . $rest,
|
||||
'anschliessend' => 'Anschließend, ' . $datePhraseLower . ', ' . $rest,
|
||||
];
|
||||
} elseif ($dayGap !== null && $dayGap <= 10) {
|
||||
$variantMap = [
|
||||
'plain' => $plain,
|
||||
'wenigSpaeter' => 'Wenig später, ' . $datePhraseLower . ', ' . $rest,
|
||||
'etwasSpaeter' => 'Etwas später, ' . $datePhraseLower . ', ' . $rest,
|
||||
'anschliessend' => 'Anschließend, ' . $datePhraseLower . ', ' . $rest,
|
||||
];
|
||||
} else {
|
||||
// Groesserer oder unbekannter Abstand: keine "bald danach"-Phrasen.
|
||||
$variantMap = [
|
||||
'plain' => $plain,
|
||||
'spaeterImMonat' => 'Später im Monat, ' . $datePhraseLower . ', ' . $rest,
|
||||
'weitererVerlauf' => 'Im weiteren Monatsverlauf, ' . $datePhraseLower . ', ' . $rest,
|
||||
];
|
||||
}
|
||||
|
||||
$labels = array_keys($variantMap);
|
||||
if ($lastLeadInLabel !== null) {
|
||||
$withoutLast = array_values(array_filter($labels, static fn (string $label): bool => $label !== $lastLeadInLabel));
|
||||
if ($withoutLast !== []) {
|
||||
$labels = $withoutLast;
|
||||
}
|
||||
}
|
||||
$chosenLabel = $seededPick($labels, $seedKey . '#' . $index, $labels[0]);
|
||||
$lastLeadInLabel = $chosenLabel;
|
||||
return $variantMap[$chosenLabel];
|
||||
};
|
||||
|
||||
$describeEvents = static function (array $events) use ($naturalJoin): string {
|
||||
$parts = [];
|
||||
foreach ($events as $event) {
|
||||
$label = trim((string) ($event['event'] ?? ''));
|
||||
@@ -852,20 +968,42 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
}
|
||||
$parts[] = $label . ' am ' . $date;
|
||||
}
|
||||
return implode(', ', $parts);
|
||||
return $naturalJoin($parts);
|
||||
};
|
||||
|
||||
$pickRandomText = static function (array $options, string $fallback): string {
|
||||
$options = array_values(array_filter($options, static fn ($option): bool => trim((string) $option) !== ''));
|
||||
if ($options === []) {
|
||||
return $fallback;
|
||||
// Wandelt das Tabellen-Label "Maximum Draconiden - DRA - ZHR ca. 20-700"
|
||||
// (fuer CSV/Tabelle/Kalender gedacht) in Fliesstext um: "das Maximum der
|
||||
// Draconiden (DRA, ZHR ca. 20-700)". Das zugrunde liegende Datenformat
|
||||
// bleibt dabei unveraendert.
|
||||
$formatMeteorLabel = static function (string $label): string {
|
||||
$segments = array_map('trim', explode(' - ', $label));
|
||||
if ($segments === [] || !str_starts_with($segments[0], 'Maximum ')) {
|
||||
return $label;
|
||||
}
|
||||
$designation = substr($segments[0], strlen('Maximum '));
|
||||
$extra = implode(', ', array_slice($segments, 1));
|
||||
$formatted = $designation === 'Meteorstrom'
|
||||
? 'das Maximum des Meteorstroms'
|
||||
: 'das Maximum der ' . $designation;
|
||||
return $formatted . ($extra !== '' ? ' (' . $extra . ')' : '');
|
||||
};
|
||||
|
||||
try {
|
||||
return (string) $options[random_int(0, count($options) - 1)];
|
||||
} catch (Throwable) {
|
||||
return (string) $options[array_rand($options)];
|
||||
$describeMeteorEvents = static function (array $events) use ($naturalJoin, $formatMeteorLabel): string {
|
||||
$parts = [];
|
||||
foreach ($events as $event) {
|
||||
$label = trim((string) ($event['event'] ?? ''));
|
||||
$date = trim((string) ($event['date'] ?? ''));
|
||||
if ($label === '' || $date === '') {
|
||||
continue;
|
||||
}
|
||||
$segments = array_map('trim', explode(' - ', $label));
|
||||
if ($segments !== [] && str_starts_with($segments[0], 'Maximum ')) {
|
||||
$parts[] = $formatMeteorLabel($label) . ' am ' . $date;
|
||||
continue;
|
||||
}
|
||||
$parts[] = $label . ' am ' . $date;
|
||||
}
|
||||
return $naturalJoin($parts);
|
||||
};
|
||||
|
||||
$buildSeasonNarrative = static function (array $event): ?string {
|
||||
@@ -892,7 +1030,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
if ($seasonEvents !== []) {
|
||||
$seasonNarrative = $buildSeasonNarrative($seasonEvents[0]);
|
||||
if ($seasonNarrative !== null) {
|
||||
$paragraphs[] = $seasonNarrative;
|
||||
$addParagraph($seasonNarrative);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -925,6 +1063,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
));
|
||||
|
||||
$moonSentences = [];
|
||||
$moonEventIndex = 0;
|
||||
|
||||
// 1. Mondphasen in einem natürlichen Satz zusammenfassen
|
||||
if ($phaseEvents !== []) {
|
||||
@@ -942,7 +1081,8 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
if ($phaseParts !== []) {
|
||||
$lastPhase = array_pop($phaseParts);
|
||||
$phaseList = $phaseParts !== [] ? implode(', ', $phaseParts) . ' und ' . $lastPhase : $lastPhase;
|
||||
$moonSentences[] = 'Die Mondphasen im Überblick: ' . $phaseList . '.';
|
||||
// rtrim gegen doppelten Punkt, da shortDate() z. B. "12.04." liefert
|
||||
$moonSentences[] = 'Die Mondphasen im Überblick: ' . rtrim($phaseList, '.') . '.';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -956,6 +1096,31 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
$byDate[$date][] = $ne;
|
||||
}
|
||||
|
||||
// Fasst Saetze desselben Tages ohne Datumswiederholung an ("Am selben Tag ...")
|
||||
// statt mit einer Sequenz-Phrase wie "Anschliessend", die faelschlich Zeitfolge suggeriert.
|
||||
$previousMoonSentenceDay = null;
|
||||
$pushMoonSentence = static function (string $dayKey, string $sentence, string $originalPrefix, string $datePhraseLower) use (&$moonSentences, &$moonEventIndex, &$previousMoonSentenceDay, $varyLeadIn, $seededPick, $dayNumberOf): void {
|
||||
if ($moonEventIndex > 0 && $dayKey === $previousMoonSentenceDay && str_starts_with($sentence, $originalPrefix)) {
|
||||
$rest = substr($sentence, strlen($originalPrefix));
|
||||
$sameDayVariants = [
|
||||
'Am selben Tag ' . $rest,
|
||||
'Zugleich ' . $rest,
|
||||
'Außerdem ' . $rest,
|
||||
];
|
||||
$moonSentences[] = $seededPick($sameDayVariants, 'moonSameDay#' . $moonEventIndex, $sentence);
|
||||
} else {
|
||||
$dayGap = null;
|
||||
$currentDayNumber = $dayNumberOf($dayKey);
|
||||
$previousDayNumber = $previousMoonSentenceDay !== null ? $dayNumberOf($previousMoonSentenceDay) : null;
|
||||
if ($currentDayNumber !== null && $previousDayNumber !== null) {
|
||||
$dayGap = abs($currentDayNumber - $previousDayNumber);
|
||||
}
|
||||
$moonSentences[] = $varyLeadIn($sentence, $originalPrefix, $datePhraseLower, $moonEventIndex, 'moonSentence', $dayGap);
|
||||
}
|
||||
$previousMoonSentenceDay = $dayKey;
|
||||
$moonEventIndex++;
|
||||
};
|
||||
|
||||
foreach ($byDate as $date => $dateEvents) {
|
||||
$apsis = null;
|
||||
$approaches = [];
|
||||
@@ -983,12 +1148,16 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
|
||||
$sd = $shortDate($date);
|
||||
|
||||
foreach (array_keys($occultationTargets) as $target) {
|
||||
$moonSentences[] = 'Am ' . $sd . ' bedeckt der Mond ' . $target . '.';
|
||||
if ($occultationTargets !== []) {
|
||||
// Mehrere Bedeckungen am selben Tag zu einem Satz zusammenfassen,
|
||||
// statt das Datum in getrennten Saetzen zu wiederholen.
|
||||
$sentence = 'Am ' . $sd . ' bedeckt der Mond ' . $naturalJoin(array_keys($occultationTargets)) . '.';
|
||||
$pushMoonSentence($sd, $sentence, 'Am ' . $sd . ' ', 'am ' . $sd);
|
||||
}
|
||||
|
||||
if ($goldenHandle !== null) {
|
||||
$moonSentences[] = 'Am ' . $sd . ' zeigt der Mond den Goldenen Henkel.';
|
||||
$sentence = 'Am ' . $sd . ' zeigt der Mond den Goldenen Henkel.';
|
||||
$pushMoonSentence($sd, $sentence, 'Am ' . $sd . ' ', 'am ' . $sd);
|
||||
}
|
||||
|
||||
if ($apsis !== null) {
|
||||
@@ -1006,7 +1175,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
$apsisText = 'Am ' . $sd . ' erreicht der Mond sein Apogäum' . $distanceStr . ' – er steht der Erde in diesem Monat am fernsten';
|
||||
}
|
||||
if ($approaches !== []) {
|
||||
// Begegnungen natürlich formulieren: "und zieht dabei in nur X Grad Abstand an Y vorbei"
|
||||
// Begegnungen natürlich formulieren, z. B. "und zieht dabei in nur X Grad Abstand an Y vorbei"
|
||||
$approachFragments = [];
|
||||
foreach ($approaches as $ap) {
|
||||
$apLabel = trim((string) ($ap['event'] ?? ''));
|
||||
@@ -1018,21 +1187,21 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
}
|
||||
if (str_starts_with($apLabel, 'Mond nahe ')) {
|
||||
$target = substr($apLabel, strlen('Mond nahe '));
|
||||
$approachFragments[] = $sepStr . ' an ' . $target . ' vorbei';
|
||||
$approachFragments[] = 'zieht dabei' . $sepStr . ' an ' . $target . ' vorbei';
|
||||
} elseif (str_starts_with($apLabel, 'Mond bedeckt ')) {
|
||||
$target = substr($apLabel, strlen('Mond bedeckt '));
|
||||
$approachFragments[] = ' und bedeckt dabei ' . $target;
|
||||
$approachFragments[] = 'bedeckt dabei ' . $target;
|
||||
} else {
|
||||
$approachFragments[] = $sepStr !== '' ? $sepStr . ' bei ' . $apLabel : ' bei ' . $apLabel;
|
||||
$approachFragments[] = $sepStr !== '' ? 'steht' . $sepStr . ' bei ' . $apLabel : 'steht bei ' . $apLabel;
|
||||
}
|
||||
}
|
||||
$approachClause = count($approachFragments) === 1
|
||||
? ' und zieht dabei' . $approachFragments[0]
|
||||
: ' und zieht dabei' . implode(' sowie', $approachFragments);
|
||||
$moonSentences[] = $apsisText . $approachClause . '.';
|
||||
// Natuerliche Aufzaehlung ("A und B", "A, B und C") statt "A sowie B sowie C".
|
||||
$approachClause = ' und ' . $naturalJoin($approachFragments);
|
||||
$sentence = $apsisText . $approachClause . '.';
|
||||
} else {
|
||||
$moonSentences[] = $apsisText . '.';
|
||||
$sentence = $apsisText . '.';
|
||||
}
|
||||
$pushMoonSentence($sd, $sentence, 'Am ' . $sd . ' ', 'am ' . $sd);
|
||||
} elseif ($approaches !== []) {
|
||||
foreach ($approaches as $ap) {
|
||||
$apLabel = trim((string) ($ap['event'] ?? ''));
|
||||
@@ -1049,37 +1218,51 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
$target = substr($apLabel, strlen('Mond bedeckt '));
|
||||
$apTime = trim((string) ($ap['time'] ?? ''));
|
||||
$timeClause = $apTime !== '' ? ' um ' . $apTime . ' Uhr' : '';
|
||||
$moonSentences[] = match ($eventKind) {
|
||||
$sentence = match ($eventKind) {
|
||||
'start' => 'Am ' . $apSd . $timeClause . ' beginnt die Bedeckung von ' . $target . ' durch den Mond.',
|
||||
'maximum' => 'Am ' . $apSd . $timeClause . ' erreicht die Bedeckung von ' . $target . ' durch den Mond ihr Maximum.',
|
||||
'end' => 'Am ' . $apSd . $timeClause . ' endet die Bedeckung von ' . $target . ' durch den Mond.',
|
||||
default => 'Am ' . $apSd . $timeClause . ' bedeckt der Mond ' . $target . '.',
|
||||
};
|
||||
$pushMoonSentence($apSd, $sentence, 'Am ' . $apSd . $timeClause . ' ', 'am ' . $apSd . $timeClause);
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($apLabel, 'Mond nahe ')) {
|
||||
$target = substr($apLabel, strlen('Mond nahe '));
|
||||
$moonSentences[] = 'Am ' . $apSd . ' zieht der Mond' . $sepStr . ' an ' . $target . ' vorbei.';
|
||||
$sentence = 'Am ' . $apSd . ' zieht der Mond' . $sepStr . ' an ' . $target . ' vorbei.';
|
||||
} elseif (str_starts_with($apLabel, 'Mond streift ')) {
|
||||
$target = substr($apLabel, strlen('Mond streift '));
|
||||
$moonSentences[] = 'Am ' . $apSd . ' streift der Mond ' . $target . '.';
|
||||
$sentence = 'Am ' . $apSd . ' streift der Mond ' . $target . '.';
|
||||
} elseif (str_starts_with($apLabel, 'Mond bedeckt ')) {
|
||||
$target = substr($apLabel, strlen('Mond bedeckt '));
|
||||
$moonSentences[] = 'Am ' . $apSd . ' bedeckt der Mond ' . $target . '.';
|
||||
$sentence = 'Am ' . $apSd . ' bedeckt der Mond ' . $target . '.';
|
||||
} else {
|
||||
$moonSentences[] = $apLabel . ' am ' . $apSd . ($sepStr !== '' ? ' – ' . ltrim($sepStr) : '') . '.';
|
||||
$sentence = $apLabel . ' am ' . $apSd . ($sepStr !== '' ? ' – ' . ltrim($sepStr) : '') . '.';
|
||||
}
|
||||
$pushMoonSentence($apSd, $sentence, 'Am ' . $apSd . ' ', 'am ' . $apSd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($moonSentences !== []) {
|
||||
$paragraphs[] = implode("\n", $moonSentences);
|
||||
// Fliesstext statt Zeilenliste, konsistent mit den anderen Absaetzen.
|
||||
$addParagraph(implode(' ', $moonSentences), 'Der Mond im ' . $monthName . ' ' . $year);
|
||||
}
|
||||
}
|
||||
|
||||
// Sprachliches Geschlecht: im Deutschen sind alle Planeten maskulin ("er"),
|
||||
// nur die Venus ist feminin ("sie"). Betrifft auch das Possessivpronomen,
|
||||
// da die hier vorkommenden Bezugswoerter (Bewegung, Beobachtung, Elongation
|
||||
// etc.) alle feminin sind ("seine" -> "ihre").
|
||||
$planetPronoun = static function (string $planetName): string {
|
||||
return trim($planetName) === 'Venus' ? 'sie' : 'er';
|
||||
};
|
||||
$planetPossessive = static function (string $planetName): string {
|
||||
return trim($planetName) === 'Venus' ? 'ihre' : 'seine';
|
||||
};
|
||||
|
||||
// Hilfsfunktion: natürlichen Satz aus einem Planeten-Event bauen
|
||||
$buildPlanetSentence = static function (array $event) use ($shortDate): ?string {
|
||||
$buildPlanetSentence = static function (array $event) use ($shortDate, $planetPronoun): ?string {
|
||||
$type = (string) ($event['type'] ?? '');
|
||||
$label = trim((string) ($event['event'] ?? ''));
|
||||
$date = trim((string) ($event['date'] ?? ''));
|
||||
@@ -1113,20 +1296,34 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === 'inner_planet_elongation') {
|
||||
$sepMatch = [];
|
||||
$sepStr = '';
|
||||
if (preg_match('/\(([0-9.,]+\s*Grad)\)/', $label, $sepMatch)) {
|
||||
$sepStr = ' (' . $sepMatch[1] . ')';
|
||||
$label = trim((string) preg_replace('/\s*\([0-9.,]+\s*Grad\)/', '', $label));
|
||||
}
|
||||
$isWest = str_contains($label, 'westlich') || str_contains($label, 'West');
|
||||
if ($type === 'inner_planet_event') {
|
||||
$planet = '';
|
||||
if (preg_match('/^(\S+)\s+/', $label, $m)) {
|
||||
$planet = $m[1];
|
||||
}
|
||||
$visibility = $isWest ? 'als Morgenstern am besten zu beobachten' : 'als Abendstern am besten zu beobachten';
|
||||
return 'Am ' . $sd . ' erreicht ' . ($planet ?: 'der Planet') . ' seine größte Elongation' . $sepStr . ' und ist dann ' . $visibility . '.';
|
||||
$planetOrFallback = $planet ?: 'der Planet';
|
||||
$pronoun = $planetPronoun($planet);
|
||||
|
||||
if (str_contains($label, 'Elongation')) {
|
||||
$sepMatch = [];
|
||||
$sepStr = '';
|
||||
if (preg_match('/\(([0-9.,]+\s*Grad)\)/', $label, $sepMatch)) {
|
||||
$sepStr = ' (' . $sepMatch[1] . ')';
|
||||
}
|
||||
$isWest = str_contains($label, 'westlich') || str_contains($label, 'West');
|
||||
$visibility = $isWest ? 'als Morgenstern' : 'als Abendstern';
|
||||
// Bewusst laienverstaendlich statt des Fachbegriffs "Elongation":
|
||||
// der Wert beschreibt den maximalen Winkelabstand von der Sonne.
|
||||
return 'Am ' . $sd . ' steht ' . $planetOrFallback . ' so weit wie in diesem Monat möglich von der Sonne entfernt' . $sepStr . ' und ist deshalb ' . $visibility . ' am besten zu beobachten.';
|
||||
}
|
||||
|
||||
if (str_contains($label, 'unterer Konjunktion')) {
|
||||
return 'Am ' . $sd . ' steht ' . $planetOrFallback . ' in unterer Konjunktion – ' . $pronoun . ' befindet sich dabei zwischen Erde und Sonne und ist deshalb einige Zeit lang nicht zu beobachten.';
|
||||
}
|
||||
|
||||
if (str_contains($label, 'oberer Konjunktion')) {
|
||||
return 'Am ' . $sd . ' steht ' . $planetOrFallback . ' in oberer Konjunktion – von der Erde aus gesehen steht ' . $pronoun . ' dann hinter der Sonne und ist ebenfalls nicht zu sehen.';
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === 'venus_peak_magnitude') {
|
||||
@@ -1135,7 +1332,11 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
if (preg_match('/\(([^)]+)\)/', $label, $magMatch)) {
|
||||
$magStr = ' mit ' . $magMatch[1];
|
||||
}
|
||||
return 'Am ' . $sd . ' erreicht Venus ihre größte Helligkeit' . $magStr . ' – sie ist dann selbst in der Dämmerung kaum zu übersehen.';
|
||||
$visibility = (string) ($event['visibility'] ?? '');
|
||||
$twilight = $visibility === 'morning'
|
||||
? 'in der Morgendämmerung'
|
||||
: ($visibility === 'evening' ? 'in der Abenddämmerung' : 'in der Dämmerung');
|
||||
return 'Am ' . $sd . ' erreicht Venus ihre größte Helligkeit' . $magStr . ' – sie ist dann selbst ' . $twilight . ' kaum zu übersehen.';
|
||||
}
|
||||
|
||||
if ($type === 'outer_planet_event') {
|
||||
@@ -1155,18 +1356,26 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
}
|
||||
|
||||
if ($type === 'mercury_good_visibility') {
|
||||
return 'Um den ' . $sd . ' zeigt sich Merkur günstig am Himmel und lässt sich in der Dämmerung tief über dem Horizont aufspüren.';
|
||||
$period = (string) ($event['period'] ?? '');
|
||||
$twilight = $period === 'morning'
|
||||
? 'in der Morgendämmerung'
|
||||
: ($period === 'evening' ? 'in der Abenddämmerung' : 'in der Dämmerung');
|
||||
return 'Um den ' . $sd . ' zeigt sich Merkur günstig am Himmel und lässt sich ' . $twilight . ' tief über dem Horizont aufspüren.';
|
||||
}
|
||||
|
||||
if ($type === 'planet_parade') {
|
||||
return 'Am ' . $sd . ' sind mehrere Planeten gleichzeitig am Himmel zu sehen – eine seltene Planetenparade, die einen Blick Richtung Süden lohnt.';
|
||||
return 'Am ' . $sd . ' sind mehrere Planeten gleichzeitig am Himmel zu sehen – eine Planetenparade, bei der sich ein Blick in den Himmel lohnt.';
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return 'Am ' . $sd . ' ist zu beobachten: ' . $label . '.';
|
||||
return 'Am ' . $sd . ' zeigt sich zudem: ' . $label . '.';
|
||||
};
|
||||
|
||||
$buildPlanetMotionSentence = static function (array $event) use ($shortDate): ?string {
|
||||
// Verhindert, dass zwei rückläufige (oder zwei wieder rechtläufige) Planeten
|
||||
// im selben Monat wortgleich beschrieben werden.
|
||||
$lastRetrogradeVariant = null;
|
||||
$lastProgradeVariant = null;
|
||||
$buildPlanetMotionSentence = static function (array $event) use ($shortDate, $planetPronoun, $planetPossessive, $seededPick, &$lastRetrogradeVariant, &$lastProgradeVariant): ?string {
|
||||
$type = (string) ($event['type'] ?? '');
|
||||
$label = trim((string) ($event['event'] ?? ''));
|
||||
$date = trim((string) ($event['date'] ?? ''));
|
||||
@@ -1182,13 +1391,44 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
if (preg_match('/^(\S+)\s+/', $label, $m)) {
|
||||
$planet = $m[1];
|
||||
}
|
||||
$planetOrFallback = $planet ?: 'der Planet';
|
||||
$pronoun = $planetPronoun($planet);
|
||||
$possessive = $planetPossessive($planet);
|
||||
if ($isRetrograde) {
|
||||
return 'Ab dem ' . $sd . ' läuft ' . ($planet ?: 'der Planet') . ' rückläufig – er bewegt sich scheinbar rückwärts vor dem Sternhintergrund, was seine Beobachtung besonders interessant macht.';
|
||||
$variants = [
|
||||
'bewegung' => 'Ab dem ' . $sd . ' läuft ' . $planetOrFallback . ' rückläufig – ' . $pronoun . ' bewegt sich scheinbar rückwärts vor dem Sternhintergrund, was ' . $possessive . ' Beobachtung besonders interessant macht.',
|
||||
'ueberholt' => 'Ab dem ' . $sd . ' bewegt sich ' . $planetOrFallback . ' scheinbar rückwärts vor dem Sternhintergrund – die Erde überholt ' . $planetOrFallback . ' dabei auf ihrer schnelleren Umlaufbahn.',
|
||||
'schleife' => 'Ab dem ' . $sd . ' zieht ' . $planetOrFallback . ' für einige Wochen eine kleine Schleife vor dem Sternhintergrund, gut zu verfolgen von Nacht zu Nacht.',
|
||||
];
|
||||
$labels = array_keys($variants);
|
||||
if ($lastRetrogradeVariant !== null) {
|
||||
$withoutLast = array_values(array_filter($labels, static fn (string $l): bool => $l !== $lastRetrogradeVariant));
|
||||
if ($withoutLast !== []) {
|
||||
$labels = $withoutLast;
|
||||
}
|
||||
}
|
||||
$chosen = $seededPick($labels, 'retrogradeVariant#' . $sd . '#' . $planetOrFallback, $labels[0]);
|
||||
$lastRetrogradeVariant = $chosen;
|
||||
return $variants[$chosen];
|
||||
}
|
||||
if ($isPrograde) {
|
||||
return 'Ab dem ' . $sd . ' setzt ' . ($planet ?: 'der Planet') . ' seine normale Vorwärtsbewegung am Himmel wieder fort.';
|
||||
$variants = [
|
||||
'fortsetzung' => 'Ab dem ' . $sd . ' setzt ' . $planetOrFallback . ' ' . $possessive . ' normale Vorwärtsbewegung am Himmel wieder fort.',
|
||||
'gewohnteRichtung' => 'Ab dem ' . $sd . ' wandert ' . $planetOrFallback . ' wieder in die gewohnte Richtung vor dem Sternhintergrund.',
|
||||
'endeSchleife' => 'Ab dem ' . $sd . ' beendet ' . $planetOrFallback . ' ' . $possessive . ' Rückwärtsschleife und zieht wieder normal durch die Sternbilder.',
|
||||
];
|
||||
$labels = array_keys($variants);
|
||||
if ($lastProgradeVariant !== null) {
|
||||
$withoutLast = array_values(array_filter($labels, static fn (string $l): bool => $l !== $lastProgradeVariant));
|
||||
if ($withoutLast !== []) {
|
||||
$labels = $withoutLast;
|
||||
}
|
||||
return 'Am ' . $sd . ' wird ' . ($planet ?: 'der Planet') . ' stationär und ändert seine Bewegungsrichtung am Himmel.';
|
||||
}
|
||||
$chosen = $seededPick($labels, 'progradeVariant#' . $sd . '#' . $planetOrFallback, $labels[0]);
|
||||
$lastProgradeVariant = $chosen;
|
||||
return $variants[$chosen];
|
||||
}
|
||||
return 'Am ' . $sd . ' wird ' . $planetOrFallback . ' stationär und ändert ' . $possessive . ' Bewegungsrichtung am Himmel.';
|
||||
}
|
||||
|
||||
if ($type === 'planet_constellation_change') {
|
||||
@@ -1213,7 +1453,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
}
|
||||
}
|
||||
|
||||
return 'Am ' . $sd . ' ist zu beobachten: ' . $label . '.';
|
||||
return 'Am ' . $sd . ' zeigt sich zudem: ' . $label . '.';
|
||||
};
|
||||
|
||||
$planetObservationEvents = $findEvents(
|
||||
@@ -1221,16 +1461,35 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['inner_planet_event', 'venus_peak_magnitude', 'outer_planet_event', 'planet_conjunction', 'planet_parade', 'mercury_good_visibility', 'golden_gate_of_ecliptic'], true),
|
||||
5
|
||||
);
|
||||
// Chronologisch sortieren, damit Uebergangsphrasen wie "Anschliessend"
|
||||
// tatsaechlich die zeitliche Reihenfolge widerspiegeln.
|
||||
usort($planetObservationEvents, static fn (array $a, array $b): int => strcmp(
|
||||
(string) ($a['date'] ?? ''),
|
||||
(string) ($b['date'] ?? '')
|
||||
));
|
||||
if ($planetObservationEvents !== []) {
|
||||
$planetSentences = [];
|
||||
$planetSentenceIndex = 0;
|
||||
$previousPlanetSentenceDayNumber = null;
|
||||
foreach ($planetObservationEvents as $pe) {
|
||||
$sentence = $buildPlanetSentence($pe);
|
||||
if ($sentence !== null) {
|
||||
$planetSentences[] = $sentence;
|
||||
if ($sentence === null) {
|
||||
continue;
|
||||
}
|
||||
$rawDate = trim((string) ($pe['date'] ?? ''));
|
||||
$sd = $shortDate($rawDate);
|
||||
$currentDayNumber = $dayNumberOf($rawDate);
|
||||
$dayGap = ($currentDayNumber !== null && $previousPlanetSentenceDayNumber !== null)
|
||||
? abs($currentDayNumber - $previousPlanetSentenceDayNumber)
|
||||
: null;
|
||||
$sentence = $varyLeadIn($sentence, 'Am ' . $sd . ' ', 'am ' . $sd, $planetSentenceIndex, 'planetSentence', $dayGap);
|
||||
$planetSentences[] = $sentence;
|
||||
$planetSentenceIndex++;
|
||||
$previousPlanetSentenceDayNumber = $currentDayNumber;
|
||||
}
|
||||
if ($planetSentences !== []) {
|
||||
$paragraphs[] = implode(' ', $planetSentences);
|
||||
$addParagraph(implode(' ', $planetSentences), 'Die Planeten im ' . $monthName . ' ' . $year);
|
||||
$planetsHeadingUsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1247,7 +1506,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
'Waage' => 'die Waage', 'Skorpion' => 'den Skorpion', 'Schütze' => 'den Schützen',
|
||||
'Steinbock' => 'den Steinbock', 'Wassermann' => 'den Wassermann', 'Fische' => 'die Fische',
|
||||
'Walfisch' => 'den Walfisch', 'Orion' => 'den Orion', 'Fuhrmann' => 'den Fuhrmann',
|
||||
'Stier' => 'den Stier', 'Kassiopeia' => 'die Kassiopeia', 'Andromeda' => 'die Andromeda',
|
||||
'Kassiopeia' => 'die Kassiopeia', 'Andromeda' => 'die Andromeda',
|
||||
'Perseus' => 'den Perseus', 'Pegasus' => 'den Pegasus', 'Leier' => 'die Leier',
|
||||
'Schwan' => 'den Schwan', 'Adler' => 'den Adler', 'Schlange' => 'die Schlange',
|
||||
'Schlangenträger' => 'den Schlangenträger', 'Herkules' => 'den Herkules',
|
||||
@@ -1267,15 +1526,34 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
|
||||
$stationEvents = array_values(array_filter($planetMotionEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'outer_planet_station'));
|
||||
$changeEvents = array_values(array_filter($planetMotionEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'planet_constellation_change'));
|
||||
// Chronologisch sortieren, damit Uebergangsphrasen wie "Anschliessend"
|
||||
// tatsaechlich die zeitliche Reihenfolge widerspiegeln.
|
||||
usort($stationEvents, static fn (array $a, array $b): int => strcmp(
|
||||
(string) ($a['date'] ?? ''),
|
||||
(string) ($b['date'] ?? '')
|
||||
));
|
||||
|
||||
$motionSentences = [];
|
||||
|
||||
// Rückläufigkeits-Ereignisse einzeln als Satz
|
||||
// Rückläufigkeits-Ereignisse einzeln als Satz, mit variierten Satzanfaengen
|
||||
$stationSentenceIndex = 0;
|
||||
$previousStationSentenceDayNumber = null;
|
||||
foreach ($stationEvents as $se) {
|
||||
$sentence = $buildPlanetMotionSentence($se);
|
||||
if ($sentence !== null) {
|
||||
$motionSentences[] = $sentence;
|
||||
if ($sentence === null) {
|
||||
continue;
|
||||
}
|
||||
$rawDate = trim((string) ($se['date'] ?? ''));
|
||||
$sd = $shortDate($rawDate);
|
||||
$currentDayNumber = $dayNumberOf($rawDate);
|
||||
$dayGap = ($currentDayNumber !== null && $previousStationSentenceDayNumber !== null)
|
||||
? abs($currentDayNumber - $previousStationSentenceDayNumber)
|
||||
: null;
|
||||
$sentence = $varyLeadIn($sentence, 'Ab dem ' . $sd . ' ', 'ab dem ' . $sd, $stationSentenceIndex, 'stationSentence', $dayGap);
|
||||
$sentence = $varyLeadIn($sentence, 'Am ' . $sd . ' ', 'am ' . $sd, $stationSentenceIndex, 'stationSentence', $dayGap);
|
||||
$motionSentences[] = $sentence;
|
||||
$stationSentenceIndex++;
|
||||
$previousStationSentenceDayNumber = $currentDayNumber;
|
||||
}
|
||||
|
||||
// Sternbild-Wechsel: nach Planet gruppieren, kompakt als eine Aussage
|
||||
@@ -1322,7 +1600,11 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
}
|
||||
|
||||
if ($motionSentences !== []) {
|
||||
$paragraphs[] = implode(' ', $motionSentences);
|
||||
$addParagraph(
|
||||
implode(' ', $motionSentences),
|
||||
$planetsHeadingUsed ? null : 'Die Planeten im ' . $monthName . ' ' . $year
|
||||
);
|
||||
$planetsHeadingUsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1332,19 +1614,19 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
4
|
||||
);
|
||||
if ($cometEvents !== []) {
|
||||
$cometIntro = $pickRandomText([
|
||||
$cometIntro = $seededPick([
|
||||
'Bei den Kometen sind in diesem Monat besonders',
|
||||
'Auch bei den Kometen lohnt sich in diesem Monat der Blick auf',
|
||||
'Unter den Kometen fallen in diesem Monat vor allem',
|
||||
'Kometenfreunde können sich in diesem Monat besonders für',
|
||||
], 'Bei den Kometen sind in diesem Monat besonders');
|
||||
$cometOutro = $pickRandomText([
|
||||
], 'cometIntro', 'Bei den Kometen sind in diesem Monat besonders');
|
||||
$cometOutro = $seededPick([
|
||||
'interessant.',
|
||||
'beobachtenswert.',
|
||||
'bemerkenswert.',
|
||||
'einen genaueren Blick wert.',
|
||||
], 'interessant.');
|
||||
$paragraphs[] = $cometIntro . ' ' . $describeEvents($cometEvents) . ' ' . $cometOutro;
|
||||
], 'cometOutro', 'interessant.');
|
||||
$addParagraph($cometIntro . ' ' . $describeEvents($cometEvents) . ' ' . $cometOutro);
|
||||
}
|
||||
|
||||
$eclipseEvents = $findEvents(
|
||||
@@ -1353,13 +1635,13 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
3
|
||||
);
|
||||
if ($eclipseEvents !== []) {
|
||||
$eclipseIntro = $pickRandomText([
|
||||
$eclipseIntro = $seededPick([
|
||||
'Besondere Aufmerksamkeit verdienen außerdem die Finsternistermine.',
|
||||
'Auch die Finsternisse setzen in diesem Monat markante Akzente.',
|
||||
'Ein weiterer Höhepunkt können in diesem Monat die Finsternistermine sein.',
|
||||
'Nicht zuletzt lohnt sich in diesem Monat auch ein Blick auf mögliche Finsternisse.',
|
||||
], 'Besondere Aufmerksamkeit verdienen ausserdem die Finsternistermine.');
|
||||
$paragraphs[] = $eclipseIntro . ' In diesem Monat stechen ' . $describeEvents($eclipseEvents) . ' hervor.';
|
||||
], 'eclipseIntro', 'Besondere Aufmerksamkeit verdienen außerdem die Finsternistermine.');
|
||||
$addParagraph($eclipseIntro . ' In diesem Monat stechen ' . $describeEvents($eclipseEvents) . ' hervor.');
|
||||
}
|
||||
|
||||
$meteorCandidates = array_values(array_filter(
|
||||
@@ -1394,27 +1676,83 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
}
|
||||
|
||||
if ($meteorEvents !== []) {
|
||||
$meteorIntro = $pickRandomText([
|
||||
$meteorIntro = $seededPick([
|
||||
'Freunde von Meteoren sollten zudem die Aktivität der Meteorströme im Blick behalten.',
|
||||
'Auch die Meteorströme verdienen in diesem Monat Aufmerksamkeit.',
|
||||
'Wer nach Sternschnuppen Ausschau halten möchte, sollte auch die Meteorströme beachten.',
|
||||
'Ein Blick auf die Meteorströme kann sich in diesem Monat ebenfalls lohnen.',
|
||||
], 'Freunde von Meteoren sollten zudem die Aktivität der Meteorströme im Blick behalten.');
|
||||
$paragraphs[] = $meteorIntro . ' Erwähnenswert sind besonders ' . $describeEvents($meteorEvents) . '.';
|
||||
], 'meteorIntro', 'Freunde von Meteoren sollten zudem die Aktivität der Meteorströme im Blick behalten.');
|
||||
$addParagraph($meteorIntro . ' Erwähnenswert sind besonders ' . $describeMeteorEvents($meteorEvents) . '.');
|
||||
}
|
||||
|
||||
if (count($paragraphs) === 1 && $filteredEvents !== []) {
|
||||
$highlights = array_slice($filteredEvents, 0, 4);
|
||||
$highlightIntro = $pickRandomText([
|
||||
$highlightIntro = $seededPick([
|
||||
'Zu den auffälligen Terminen des Monats gehören',
|
||||
'Unter den markanten Terminen dieses Monats finden sich',
|
||||
'Im Monatsverlauf fallen vor allem',
|
||||
'Besonders bemerkenswert im Monatskalender sind',
|
||||
], 'Zu den auffaelligen Terminen des Monats gehoeren');
|
||||
$paragraphs[] = $highlightIntro . ' ' . $describeEvents($highlights) . '.';
|
||||
], 'highlightIntro', 'Zu den auffälligen Terminen des Monats gehören');
|
||||
$addParagraph($highlightIntro . ' ' . $describeEvents($highlights) . '.');
|
||||
}
|
||||
|
||||
return implode("\n\n", $paragraphs);
|
||||
// Aufhaenger: das markanteste Ereignis des Monats vorab kurz ankuendigen,
|
||||
// statt immer nur mit dem generischen Himmelsueberblick zu beginnen.
|
||||
$headlineCandidate = null;
|
||||
if ($eclipseEvents !== []) {
|
||||
$headlineCandidate = $eclipseEvents[0];
|
||||
} elseif (($matches = array_values(array_filter($filteredEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'moon_occultation' && !in_array((string) ($e['event_kind'] ?? ''), ['start', 'end'], true)))) !== []) {
|
||||
$headlineCandidate = $matches[0];
|
||||
} elseif (($matches = array_values(array_filter($planetObservationEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'outer_planet_event' && str_contains((string) ($e['event'] ?? ''), 'Opposition')))) !== []) {
|
||||
$headlineCandidate = $matches[0];
|
||||
} elseif (($matches = array_values(array_filter($planetObservationEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'golden_gate_of_ecliptic'))) !== []) {
|
||||
$headlineCandidate = $matches[0];
|
||||
} elseif ($meteorEvents !== [] && max((int) ($meteorEvents[0]['zhr_min_estimate'] ?? 0), (int) ($meteorEvents[0]['zhr_max_estimate'] ?? 0)) >= 50) {
|
||||
$headlineCandidate = $meteorEvents[0];
|
||||
} elseif (($matches = array_values(array_filter($planetObservationEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'planet_conjunction'))) !== []) {
|
||||
$headlineCandidate = $matches[0];
|
||||
} elseif (($matches = array_values(array_filter($planetObservationEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'venus_peak_magnitude'))) !== []) {
|
||||
$headlineCandidate = $matches[0];
|
||||
}
|
||||
|
||||
if ($headlineCandidate !== null) {
|
||||
$headlineDate = trim((string) ($headlineCandidate['date'] ?? ''));
|
||||
$headlineLabel = trim((string) ($headlineCandidate['event'] ?? ''));
|
||||
// Fuer Meteorstroeme das rohe Tabellen-Label ("Maximum X - CODE - ZHR ca. Y")
|
||||
// in Fliesstext umwandeln, wie im Meteor-Absatz.
|
||||
if ((string) ($headlineCandidate['type'] ?? '') === 'meteor_shower_peak') {
|
||||
$headlineLabel = $formatMeteorLabel($headlineLabel);
|
||||
}
|
||||
// Bedeckungen mit Kontaktphase ("Mond bedeckt Jupiter – Groesste Bedeckung")
|
||||
// sind ein einmaliges Ereignis an einem Tag, kein Ereignis mit eigenem
|
||||
// Maximum ueber mehrere Tage - den Kontaktphasen-Zusatz daher weglassen.
|
||||
if ((string) ($headlineCandidate['type'] ?? '') === 'moon_occultation') {
|
||||
$headlineLabel = trim(explode(' – ', $headlineLabel, 2)[0]);
|
||||
}
|
||||
$headlineText = $headlineLabel . ' am ' . rtrim($shortDate($headlineDate), '.');
|
||||
if ($headlineLabel !== '' && $headlineDate !== '') {
|
||||
$headlineLeadIn = $seededPick([
|
||||
'Der Höhepunkt des Monats',
|
||||
'Am meisten Aufmerksamkeit verdient in diesem Monat',
|
||||
'Nicht verpassen',
|
||||
'Besonders hervorzuheben in diesem Monat',
|
||||
], 'headlineLeadIn', 'Der Höhepunkt des Monats');
|
||||
array_unshift(
|
||||
$paragraphs,
|
||||
['heading' => null, 'text' => $headlineLeadIn . ': ' . $headlineText . '.', 'emphasize' => true]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$closingSentence = $seededPick([
|
||||
'Ein Blick zum Himmel lohnt sich in diesem Monat gleich mehrfach – denn das Wesentliche spielt sich dort oben ab.',
|
||||
'Es gibt also einige gute Gründe, in diesem Monat öfter nach oben zu schauen – dort oben ist einiges los.',
|
||||
'Wer sich Zeit für den Nachthimmel nimmt, wird in diesem Monat mehrfach belohnt – denn das Wichtigste findet dort oben statt.',
|
||||
'Für Sternfreunde bietet dieser Monat damit reichlich Gelegenheit zum Beobachten – schließlich passiert das Wesentliche am Himmel.',
|
||||
], 'closingSentence', 'Ein Blick zum Himmel lohnt sich in diesem Monat gleich mehrfach – denn das Wesentliche spielt sich dort oben ab.');
|
||||
$addParagraph($closingSentence);
|
||||
|
||||
return $paragraphs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2426,6 +2764,7 @@ foreach ($venusPeakMagnitudeEvents as $venusPeakMagnitudeEvent) {
|
||||
'date' => (string) ($venusPeakMagnitudeEvent['local_date'] ?? ''),
|
||||
'time' => (string) ($venusPeakMagnitudeEvent['local_time'] ?? ''),
|
||||
'local_iso' => (string) ($venusPeakMagnitudeEvent['local_iso'] ?? ''),
|
||||
'visibility' => (string) ($venusPeakMagnitudeEvent['visibility'] ?? ''),
|
||||
'type' => 'venus_peak_magnitude',
|
||||
];
|
||||
}
|
||||
@@ -2584,6 +2923,7 @@ foreach ($mercuryGoodVisibilityEvents as $visibilityEvent) {
|
||||
'local_iso' => (string) ($visibilityEvent['local_iso'] ?? ''),
|
||||
'end_local_iso' => (string) ($visibilityEvent['end_local_iso'] ?? ''),
|
||||
'duration_minutes' => isset($visibilityEvent['duration_minutes']) ? (int) $visibilityEvent['duration_minutes'] : null,
|
||||
'period' => (string) ($visibilityEvent['period'] ?? ''),
|
||||
'type' => 'mercury_good_visibility',
|
||||
];
|
||||
}
|
||||
@@ -3031,7 +3371,21 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
||||
</div>
|
||||
</div>
|
||||
<div class="monatskarte-footer">
|
||||
<p class="monatsnarrativ-text"><?= htmlspecialchars($monthNarrativeText, ENT_QUOTES, 'UTF-8') ?></p>
|
||||
<div class="monatsnarrativ-text">
|
||||
<?php foreach ($monthNarrativeText as $narrativeBlock): ?>
|
||||
<?php if (!empty($narrativeBlock['heading'])): ?>
|
||||
<h4><?= htmlspecialchars((string) $narrativeBlock['heading'], ENT_QUOTES, 'UTF-8') ?></h4>
|
||||
<?php endif; ?>
|
||||
<?php foreach (explode("\n\n", (string) ($narrativeBlock['text'] ?? '')) as $narrativeSubParagraph): ?>
|
||||
<?php $narrativeSubParagraph = trim($narrativeSubParagraph); ?>
|
||||
<?php if ($narrativeSubParagraph !== ''): ?>
|
||||
<?php // "highlight"-Marker nur fuer den HTML-/Joomla-Export (siehe narrativeSectionsHtml() im JS);
|
||||
// auf der Seite selbst bleibt der Absatz optisch normal. ?>
|
||||
<p<?= !empty($narrativeBlock['emphasize']) ? ' class="monatsnarrativ-highlight"' : '' ?>><?= htmlspecialchars($narrativeSubParagraph, ENT_QUOTES, 'UTF-8') ?></p>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -3128,7 +3482,9 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
||||
</div>
|
||||
</div>
|
||||
<div class="monatskarte-footer">
|
||||
<p class="monatsnarrativ-text">Bitte zuerst Jahr und Monat auswählen und dann auf „Monatsvorhersage berechnen“ klicken.</p>
|
||||
<div class="monatsnarrativ-text">
|
||||
<p>Bitte zuerst Jahr und Monat auswählen und dann auf „Monatsvorhersage berechnen“ klicken.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
@@ -3384,7 +3740,24 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
||||
|
||||
.monatsnarrativ-text {
|
||||
font-size: 1.02rem;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.monatsnarrativ-text p {
|
||||
margin: 0 0 12px;
|
||||
color: rgba(222, 232, 255, 0.86);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.monatsnarrativ-text p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.monatsnarrativ-text h4 {
|
||||
margin: 20px 0 8px;
|
||||
}
|
||||
|
||||
.monatsnarrativ-text h4:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.monatskarte-stage {
|
||||
@@ -6329,10 +6702,27 @@ function monthChartCometRaDec(comet, date) {
|
||||
}
|
||||
}
|
||||
|
||||
function narrativeSectionsHtml() {
|
||||
const container = overviewEl.querySelector('.monatsnarrativ-text');
|
||||
if (!container) {
|
||||
return '';
|
||||
}
|
||||
return Array.from(container.children).map(function (el) {
|
||||
const text = (el.textContent || '').trim();
|
||||
if (text === '') {
|
||||
return '';
|
||||
}
|
||||
const tag = el.tagName === 'H4' ? 'h4' : 'p';
|
||||
// Der Aufhaenger-Satz wird nur im Export fett hervorgehoben, nicht auf der Seite selbst.
|
||||
const inner = el.classList.contains('monatsnarrativ-highlight')
|
||||
? '<strong>' + escapeHtml(text) + '</strong>'
|
||||
: escapeHtml(text);
|
||||
return '<' + tag + '>' + inner + '</' + tag + '>\n';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function overviewHtml(images) {
|
||||
const monthLabel = (overviewEl.querySelector('.monatskarte-header p')?.textContent || '').trim();
|
||||
const narrative = (overviewEl.querySelector('.monatsnarrativ-text')?.textContent || '').trim();
|
||||
const escapedNarrative = escapeHtml(narrative).replace(/\r?\n/g, '<br>\n');
|
||||
const imageMarkup = await Promise.all(images.map(async function (image) {
|
||||
const scaledImage = await scaleImageBlob(image.content, htmlImageMaxWidth);
|
||||
const imageDataUrl = await blobToDataUrl(scaledImage);
|
||||
@@ -6341,7 +6731,7 @@ function monthChartCometRaDec(comet, date) {
|
||||
}));
|
||||
|
||||
return '<h3>' + escapeHtml(monthLabel) + '</h3>\n'
|
||||
+ '<p>' + escapedNarrative + '</p>\n'
|
||||
+ narrativeSectionsHtml()
|
||||
+ imageMarkup.join('')
|
||||
+ eventTableHtml()
|
||||
+ '<p>Alle Zeiten gelten für MEZ/MESZ.</p>\n'
|
||||
|
||||
@@ -4509,12 +4509,14 @@ def action_venus_peak_magnitude_for_month(args: list[str]) -> dict:
|
||||
break
|
||||
|
||||
if local_dt >= local_start:
|
||||
elongation_info = astronomy.Elongation(astronomy.Body.Venus, event.time)
|
||||
events.append({
|
||||
"planet_key": "venus",
|
||||
"planet_label": "Venus",
|
||||
"kind": "peak_magnitude",
|
||||
"label": "Venus in größter Helligkeit",
|
||||
"mag": float(event.mag),
|
||||
"visibility": "morning" if elongation_info.visibility == astronomy.Visibility.Morning else "evening",
|
||||
"utc_iso": dt_utc.isoformat().replace("+00:00", "Z"),
|
||||
"local_iso": local_dt.isoformat(),
|
||||
"local_date": local_dt.strftime("%d.%m.%Y"),
|
||||
|
||||
Reference in New Issue
Block a user