1use std::fmt::{Display, Write as _};
18
19pub fn number_with_delimiter(value: impl Display) -> String {
31 number_with_delimiter_text(value.to_string())
32}
33
34fn number_with_delimiter_text(text: String) -> String {
35 with_delimiter(&text)
36}
37
38pub fn number_with_precision(value: impl Display, precision: usize) -> String {
46 number_with_precision_text(value.to_string(), precision)
47}
48
49fn number_with_precision_text(text: String, precision: usize) -> String {
50 number(&text).map_or(text, |n| format!("{n:.precision$}"))
51}
52
53pub fn number_to_currency(value: impl Display, unit: &str) -> String {
63 number_to_currency_text(value.to_string(), unit)
64}
65
66fn number_to_currency_text(text: String, unit: &str) -> String {
67 match number(&text) {
68 Some(n) => {
69 let sign = if n < 0.0 && (n * 100.0).round() != 0.0 { "-" } else { "" };
70 format!("{sign}{unit}{}", with_delimiter(&format!("{:.2}", n.abs())))
71 }
72 None => text,
73 }
74}
75
76pub fn number_to_percentage(value: impl Display, precision: usize) -> String {
84 number_to_percentage_text(value.to_string(), precision)
85}
86
87fn number_to_percentage_text(text: String, precision: usize) -> String {
88 number(&text).map_or(text, |n| format!("{n:.precision$}%"))
89}
90
91pub fn number_to_human_size(value: impl Display) -> String {
99 number_to_human_size_text(value.to_string())
100}
101
102fn number_to_human_size_text(text: String) -> String {
103 match number(&text) {
104 Some(n) if n >= 0.0 => crate::storage::human_size(n as u64),
105 _ => text,
106 }
107}
108
109pub fn number_to_human(value: impl Display) -> String {
118 number_to_human_text(value.to_string())
119}
120
121fn number_to_human_text(text: String) -> String {
122 number(&text).map_or(text, human)
123}
124
125pub fn time_ago_in_words(value: impl Display) -> String {
140 time_ago_in_words_text(value.to_string())
141}
142
143fn time_ago_in_words_text(text: String) -> String {
144 parse_time(&text).map_or(text, |time| distance_in_words(time, crate::now()))
145}
146
147pub fn distance_of_time_in_words(from: impl Display, to: impl Display) -> String {
159 distance_text(from.to_string(), &to.to_string())
160}
161
162fn distance_text(text: String, to: &str) -> String {
163 match (parse_time(&text), parse_time(to)) {
164 (Some(from), Some(to)) => distance_in_words(from, to),
165 _ => text,
166 }
167}
168
169pub fn strftime(value: impl Display, format: &str) -> String {
187 strftime_text(value.to_string(), format)
188}
189
190fn strftime_text(text: String, format: &str) -> String {
191 parse_time(&text).map_or(text, |time| format_time(time, format))
192}
193
194pub fn excerpt(value: impl Display, phrase: &str, radius: usize) -> String {
204 excerpt_text(value.to_string(), phrase, radius)
205}
206
207fn excerpt_text(text: String, phrase: &str, radius: usize) -> String {
208 let text: Vec<char> = text.chars().collect();
209 let Some(start) = find_ignore_case(&text, phrase, 0) else {
210 return String::new();
211 };
212 let end = start + phrase.chars().count();
213 let (from, to) = (start.saturating_sub(radius), (end + radius).min(text.len()));
214 let mut out = String::new();
215 if from > 0 {
216 out.push_str("...");
217 }
218 out.extend(&text[from..to]);
219 if to < text.len() {
220 out.push_str("...");
221 }
222 out
223}
224
225pub fn highlight(value: impl Display, phrase: &str) -> String {
238 highlight_text(value.to_string(), phrase)
239}
240
241fn highlight_text(text: String, phrase: &str) -> String {
242 let text: Vec<char> = text.chars().collect();
243 let length = phrase.chars().count();
244 let mut out = String::with_capacity(text.len() + 16);
245 let mut at = 0;
246 while let Some(start) = (length > 0).then(|| find_ignore_case(&text, phrase, at)).flatten() {
247 escape_into(&mut out, &text[at..start]);
248 out.push_str("<mark>");
249 escape_into(&mut out, &text[start..start + length]);
250 out.push_str("</mark>");
251 at = start + length;
252 }
253 escape_into(&mut out, &text[at..]);
254 out
255}
256
257pub fn word_wrap(value: impl Display, width: usize) -> String {
268 word_wrap_text(value.to_string(), width)
269}
270
271fn word_wrap_text(text: String, width: usize) -> String {
272 let mut lines = Vec::new();
273 for paragraph in text.split('\n') {
274 let mut line = String::new();
275 for word in paragraph.split_whitespace() {
276 if !line.is_empty() && line.chars().count() + 1 + word.chars().count() > width {
277 lines.push(std::mem::take(&mut line));
278 }
279 if !line.is_empty() {
280 line.push(' ');
281 }
282 line.push_str(word);
283 }
284 lines.push(line);
285 }
286 lines.join("\n")
287}
288
289pub fn class_names(classes: &[(&str, bool)]) -> String {
301 let mut out = String::new();
302 for &(name, on) in classes {
303 if on && !name.is_empty() {
304 if !out.is_empty() {
305 out.push(' ');
306 }
307 out.push_str(name);
308 }
309 }
310 out
311}
312
313pub fn current_page(current: &str, url: &str) -> bool {
338 let (current_host, current_path, current_query) = split_url(current);
339 let (host, path, query) = split_url(url);
340 let same_host = match (current_host, host) {
341 (Some(current_host), Some(host)) => current_host.eq_ignore_ascii_case(host),
342 _ => true,
343 };
344 same_host && current_path == path && query.is_none_or(|query| current_query == Some(query))
345}
346
347fn split_url(url: &str) -> (Option<&str>, &str, Option<&str>) {
349 let url = url.split('#').next().unwrap_or_default();
350 let (host, rest) = match url.split_once("://") {
351 Some((_, after)) => {
352 let end = after.find(['/', '?']).unwrap_or(after.len());
353 (Some(&after[..end]), &after[end..])
354 }
355 None => (None, url),
356 };
357 let (path, query) = match rest.split_once('?') {
358 Some((path, query)) => (path, Some(query).filter(|query| !query.is_empty())),
359 None => (rest, None),
360 };
361 let path = path.trim_end_matches('/');
362 (host, if path.is_empty() { "/" } else { path }, query)
363}
364
365pub(crate) fn number(text: &str) -> Option<f64> {
366 text.trim().parse::<f64>().ok().filter(|n| n.is_finite())
367}
368
369fn with_delimiter(text: &str) -> String {
371 delimit(text, ",", ".")
372}
373
374pub(crate) fn delimit(text: &str, delimiter: &str, separator: &str) -> String {
376 if number(text).is_none() {
377 return text.to_owned();
378 }
379 let text = text.trim();
380 let (sign, rest) = text.strip_prefix('-').map_or(("", text), |rest| ("-", rest));
381 let (int, frac) = rest.split_once('.').map_or((rest, None), |(int, frac)| (int, Some(frac)));
382 if !int.bytes().all(|b| b.is_ascii_digit()) {
383 return text.to_owned();
384 }
385 let mut out = String::from(sign);
386 for (i, digit) in int.chars().enumerate() {
387 if i > 0 && (int.len() - i) % 3 == 0 {
388 out.push_str(delimiter);
389 }
390 out.push(digit);
391 }
392 if let Some(frac) = frac {
393 out.push_str(separator);
394 out.push_str(frac);
395 }
396 out
397}
398
399fn human(n: f64) -> String {
400 const UNITS: [&str; 5] = ["Thousand", "Million", "Billion", "Trillion", "Quadrillion"];
401 let mut exponent = 0;
402 let mut value = n.abs();
403 while value >= 999.5 && exponent < UNITS.len() {
404 value /= 1000.0;
405 exponent += 1;
406 }
407 let digits = if value >= 99.95 {
408 0
409 } else if value >= 9.995 {
410 1
411 } else {
412 2
413 };
414 let rounded = format!("{value:.digits$}");
415 let rounded = if rounded.contains('.') { rounded.trim_end_matches('0').trim_end_matches('.') } else { &rounded };
416 let sign = if n < 0.0 { "-" } else { "" };
417 match exponent {
418 0 => format!("{sign}{rounded}"),
419 _ => format!("{sign}{rounded} {}", UNITS[exponent - 1]),
420 }
421}
422
423pub(crate) fn parse_time(text: &str) -> Option<i64> {
426 let text = text.trim();
427 if let Ok(seconds) = text.parse::<i64>() {
428 return Some(seconds);
429 }
430 let number = |part: &str| -> Option<i64> {
431 (!part.is_empty() && part.bytes().all(|b| b.is_ascii_digit())).then(|| part.parse().ok()).flatten()
432 };
433 let (date, rest) = text.split_at(text.len().min(10));
434 let mut ymd = date.splitn(3, '-');
435 let (year, month, day) = (number(ymd.next()?)?, number(ymd.next()?)?, number(ymd.next()?)?);
436 if date.len() != 10 || !(1..=12).contains(&month) || !(1..=31).contains(&day) {
437 return None;
438 }
439 let mut seconds = days_from_civil(year, month, day) * 86_400;
440 let rest = rest.strip_prefix(['T', ' ']).unwrap_or(rest);
441 let zone_at = rest.find(['Z', '+', '-']).unwrap_or(rest.len());
442 let (clock, zone) = rest.split_at(zone_at);
443 if !clock.is_empty() {
444 let clock = clock.split('.').next().unwrap_or(clock);
445 let mut hms = clock.split(':');
446 let (hour, minute) = (number(hms.next()?)?, number(hms.next()?)?);
447 let second = hms.next().map_or(Some(0), number)?;
448 if hms.next().is_some() || hour > 23 || minute > 59 || second > 60 {
449 return None;
450 }
451 seconds += hour * 3600 + minute * 60 + second;
452 }
453 match zone {
454 "" | "Z" => Some(seconds),
455 _ => {
456 let (sign, offset) = zone.split_at(1);
457 let (hours, minutes) = offset.split_once(':').unwrap_or((offset, "0"));
458 let offset = number(hours)? * 3600 + number(minutes)? * 60;
459 Some(if sign == "+" { seconds - offset } else { seconds + offset })
460 }
461 }
462}
463
464fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
466 let year = if month <= 2 { year - 1 } else { year };
467 let era = year.div_euclid(400);
468 let year_of_era = year - era * 400;
469 let day_of_year = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + day - 1;
470 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
471 era * 146_097 + day_of_era - 719_468
472}
473
474pub(crate) fn civil_from_days(days: i64) -> (i64, i64, i64) {
476 let days = days + 719_468;
477 let era = days.div_euclid(146_097);
478 let day_of_era = days - era * 146_097;
479 let year_of_era = (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
480 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
481 let mp = (5 * day_of_year + 2) / 153;
482 let day = day_of_year - (153 * mp + 2) / 5 + 1;
483 let month = if mp < 10 { mp + 3 } else { mp - 9 };
484 let year = year_of_era + era * 400 + i64::from(month <= 2);
485 (year, month, day)
486}
487
488pub(crate) struct DateNames<'a> {
490 pub(crate) months: [&'a str; 12],
491 pub(crate) abbr_months: [&'a str; 12],
492 pub(crate) days: [&'a str; 7],
494 pub(crate) abbr_days: [&'a str; 7],
495 pub(crate) am: &'a str,
496 pub(crate) pm: &'a str,
497}
498
499pub(crate) const ENGLISH_NAMES: DateNames<'static> = DateNames {
500 months: [
501 "January",
502 "February",
503 "March",
504 "April",
505 "May",
506 "June",
507 "July",
508 "August",
509 "September",
510 "October",
511 "November",
512 "December",
513 ],
514 abbr_months: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
515 days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
516 abbr_days: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
517 am: "AM",
518 pm: "PM",
519};
520
521fn format_time(time: i64, format: &str) -> String {
522 format_time_with(time, format, &ENGLISH_NAMES)
523}
524
525pub(crate) fn format_time_with(time: i64, format: &str, names: &DateNames<'_>) -> String {
527 let days = time.div_euclid(86_400);
528 let of_day = time.rem_euclid(86_400);
529 let (year, month, day) = civil_from_days(days);
530 let (hour, minute, second) = (of_day / 3600, of_day % 3600 / 60, of_day % 60);
531 let hour12 = if hour % 12 == 0 { 12 } else { hour % 12 };
532 let month_index = (month - 1) as usize;
533 let weekday = (days + 4).rem_euclid(7) as usize;
535 let mut out = String::new();
536 let mut chars = format.chars();
537 while let Some(c) = chars.next() {
538 if c != '%' {
539 out.push(c);
540 continue;
541 }
542 let (unpadded, directive) = match chars.next() {
543 Some('-') => (true, chars.next()),
544 other => (false, other),
545 };
546 let padded = |value: i64| if unpadded { value.to_string() } else { format!("{value:02}") };
547 let written = match directive {
548 Some('Y') => write!(out, "{year}"),
549 Some('y') => write!(out, "{:02}", year.rem_euclid(100)),
550 Some('m') => write!(out, "{}", padded(month)),
551 Some('d') => write!(out, "{}", padded(day)),
552 Some('e') => write!(out, "{day:>2}"),
553 Some('H') => write!(out, "{}", padded(hour)),
554 Some('I') => write!(out, "{}", padded(hour12)),
555 Some('M') => write!(out, "{minute:02}"),
556 Some('S') => write!(out, "{second:02}"),
557 Some('p') => write!(out, "{}", if hour < 12 { names.am } else { names.pm }),
558 Some('b') => write!(out, "{}", names.abbr_months[month_index]),
559 Some('B') => write!(out, "{}", names.months[month_index]),
560 Some('a') => write!(out, "{}", names.abbr_days[weekday]),
561 Some('A') => write!(out, "{}", names.days[weekday]),
562 Some('j') => write!(out, "{:03}", days - days_from_civil(year, 1, 1) + 1),
563 Some('F') => write!(out, "{year}-{month:02}-{day:02}"),
564 Some('T') => write!(out, "{hour:02}:{minute:02}:{second:02}"),
565 Some('z') => write!(out, "+0000"),
566 Some('%') => write!(out, "%"),
567 Some(other) => write!(out, "%{}{other}", if unpadded { "-" } else { "" }),
568 None => write!(out, "%"),
569 };
570 written.expect("writing to a String");
571 }
572 out
573}
574
575fn distance_in_words(from: i64, to: i64) -> String {
577 let (key, count) = distance_key(from, to);
578 crate::i18n::english_distance(key, count)
579}
580
581pub(crate) fn distance_key(from: i64, to: i64) -> (&'static str, i64) {
584 const HOUR: i64 = 60;
585 const DAY: i64 = 1440;
586 const MONTH: i64 = 43_200;
587 const YEAR: i64 = 525_600;
588 let minutes = ((from - to).abs() + 30) / 60;
589 let rounded = |unit: i64| (minutes + unit / 2) / unit;
590 match minutes {
591 0 => ("less_than_x_minutes", 1),
592 1..45 => ("x_minutes", minutes),
593 45..90 => ("about_x_hours", 1),
594 90..DAY => ("about_x_hours", rounded(HOUR)),
595 DAY..2520 => ("x_days", 1),
596 2520..MONTH => ("x_days", rounded(DAY)),
597 MONTH..86_400 => ("about_x_months", rounded(MONTH)),
598 86_400..YEAR => ("x_months", rounded(MONTH)),
599 _ => {
600 let (years, remainder) = (minutes / YEAR, minutes % YEAR);
601 if remainder < YEAR / 4 {
602 ("about_x_years", years)
603 } else if remainder < YEAR * 3 / 4 {
604 ("over_x_years", years)
605 } else {
606 ("almost_x_years", years + 1)
607 }
608 }
609 }
610}
611
612fn find_ignore_case(text: &[char], phrase: &str, from: usize) -> Option<usize> {
614 let phrase: Vec<char> = phrase.chars().flat_map(char::to_lowercase).collect();
615 if phrase.is_empty() || phrase.len() > text.len() {
616 return None;
617 }
618 (from..=text.len() - phrase.len()).find(|&start| {
619 text[start..start + phrase.len()].iter().flat_map(|c| c.to_lowercase()).eq(phrase.iter().copied())
620 })
621}
622
623fn escape_into(out: &mut String, text: &[char]) {
624 for c in text {
625 match c {
626 '&' => out.push_str("&"),
627 '<' => out.push_str("<"),
628 '>' => out.push_str(">"),
629 '"' => out.push_str("""),
630 '\'' => out.push_str("'"),
631 _ => out.push(*c),
632 }
633 }
634}
635
636pub fn progress_bar(id: &str, percent: i64, label: &str) -> String {
654 let percent = percent.clamp(0, 100);
655 let mut out = String::from("<div id=\"");
656 escape_into(&mut out, &id.chars().collect::<Vec<_>>());
657 write!(out, "\" class=\"progress-bar\"><progress max=\"100\" value=\"{percent}\">{percent}%</progress> <span>")
658 .expect("writing to a String");
659 escape_into(&mut out, &label.chars().collect::<Vec<_>>());
660 out.push_str("</span></div>");
661 out
662}
663
664pub const TIME_ZONES: &[&str] = &[
673 "UTC",
674 "Africa/Abidjan",
675 "Africa/Accra",
676 "Africa/Addis_Ababa",
677 "Africa/Algiers",
678 "Africa/Asmera",
679 "Africa/Bamako",
680 "Africa/Bangui",
681 "Africa/Banjul",
682 "Africa/Bissau",
683 "Africa/Blantyre",
684 "Africa/Brazzaville",
685 "Africa/Bujumbura",
686 "Africa/Cairo",
687 "Africa/Casablanca",
688 "Africa/Ceuta",
689 "Africa/Conakry",
690 "Africa/Dakar",
691 "Africa/Dar_es_Salaam",
692 "Africa/Djibouti",
693 "Africa/Douala",
694 "Africa/El_Aaiun",
695 "Africa/Freetown",
696 "Africa/Gaborone",
697 "Africa/Harare",
698 "Africa/Johannesburg",
699 "Africa/Juba",
700 "Africa/Kampala",
701 "Africa/Khartoum",
702 "Africa/Kigali",
703 "Africa/Kinshasa",
704 "Africa/Lagos",
705 "Africa/Libreville",
706 "Africa/Lome",
707 "Africa/Luanda",
708 "Africa/Lubumbashi",
709 "Africa/Lusaka",
710 "Africa/Malabo",
711 "Africa/Maputo",
712 "Africa/Maseru",
713 "Africa/Mbabane",
714 "Africa/Mogadishu",
715 "Africa/Monrovia",
716 "Africa/Nairobi",
717 "Africa/Ndjamena",
718 "Africa/Niamey",
719 "Africa/Nouakchott",
720 "Africa/Ouagadougou",
721 "Africa/Porto-Novo",
722 "Africa/Sao_Tome",
723 "Africa/Tripoli",
724 "Africa/Tunis",
725 "Africa/Windhoek",
726 "America/Adak",
727 "America/Anchorage",
728 "America/Anguilla",
729 "America/Antigua",
730 "America/Araguaina",
731 "America/Argentina/La_Rioja",
732 "America/Argentina/Rio_Gallegos",
733 "America/Argentina/Salta",
734 "America/Argentina/San_Juan",
735 "America/Argentina/San_Luis",
736 "America/Argentina/Tucuman",
737 "America/Argentina/Ushuaia",
738 "America/Aruba",
739 "America/Asuncion",
740 "America/Bahia",
741 "America/Bahia_Banderas",
742 "America/Barbados",
743 "America/Belem",
744 "America/Belize",
745 "America/Blanc-Sablon",
746 "America/Boa_Vista",
747 "America/Bogota",
748 "America/Boise",
749 "America/Buenos_Aires",
750 "America/Cambridge_Bay",
751 "America/Campo_Grande",
752 "America/Cancun",
753 "America/Caracas",
754 "America/Catamarca",
755 "America/Cayenne",
756 "America/Cayman",
757 "America/Chicago",
758 "America/Chihuahua",
759 "America/Ciudad_Juarez",
760 "America/Coral_Harbour",
761 "America/Cordoba",
762 "America/Costa_Rica",
763 "America/Coyhaique",
764 "America/Creston",
765 "America/Cuiaba",
766 "America/Curacao",
767 "America/Danmarkshavn",
768 "America/Dawson",
769 "America/Dawson_Creek",
770 "America/Denver",
771 "America/Detroit",
772 "America/Dominica",
773 "America/Edmonton",
774 "America/Eirunepe",
775 "America/El_Salvador",
776 "America/Fort_Nelson",
777 "America/Fortaleza",
778 "America/Glace_Bay",
779 "America/Godthab",
780 "America/Goose_Bay",
781 "America/Grand_Turk",
782 "America/Grenada",
783 "America/Guadeloupe",
784 "America/Guatemala",
785 "America/Guayaquil",
786 "America/Guyana",
787 "America/Halifax",
788 "America/Havana",
789 "America/Hermosillo",
790 "America/Indiana/Knox",
791 "America/Indiana/Marengo",
792 "America/Indiana/Petersburg",
793 "America/Indiana/Tell_City",
794 "America/Indiana/Vevay",
795 "America/Indiana/Vincennes",
796 "America/Indiana/Winamac",
797 "America/Indianapolis",
798 "America/Inuvik",
799 "America/Iqaluit",
800 "America/Jamaica",
801 "America/Jujuy",
802 "America/Juneau",
803 "America/Kentucky/Monticello",
804 "America/Kralendijk",
805 "America/La_Paz",
806 "America/Lima",
807 "America/Los_Angeles",
808 "America/Louisville",
809 "America/Lower_Princes",
810 "America/Maceio",
811 "America/Managua",
812 "America/Manaus",
813 "America/Marigot",
814 "America/Martinique",
815 "America/Matamoros",
816 "America/Mazatlan",
817 "America/Mendoza",
818 "America/Menominee",
819 "America/Merida",
820 "America/Metlakatla",
821 "America/Mexico_City",
822 "America/Miquelon",
823 "America/Moncton",
824 "America/Monterrey",
825 "America/Montevideo",
826 "America/Montserrat",
827 "America/Nassau",
828 "America/New_York",
829 "America/Nome",
830 "America/Noronha",
831 "America/North_Dakota/Beulah",
832 "America/North_Dakota/Center",
833 "America/North_Dakota/New_Salem",
834 "America/Ojinaga",
835 "America/Panama",
836 "America/Paramaribo",
837 "America/Phoenix",
838 "America/Port-au-Prince",
839 "America/Port_of_Spain",
840 "America/Porto_Velho",
841 "America/Puerto_Rico",
842 "America/Punta_Arenas",
843 "America/Rankin_Inlet",
844 "America/Recife",
845 "America/Regina",
846 "America/Resolute",
847 "America/Rio_Branco",
848 "America/Santarem",
849 "America/Santiago",
850 "America/Santo_Domingo",
851 "America/Sao_Paulo",
852 "America/Scoresbysund",
853 "America/Sitka",
854 "America/St_Barthelemy",
855 "America/St_Johns",
856 "America/St_Kitts",
857 "America/St_Lucia",
858 "America/St_Thomas",
859 "America/St_Vincent",
860 "America/Swift_Current",
861 "America/Tegucigalpa",
862 "America/Thule",
863 "America/Tijuana",
864 "America/Toronto",
865 "America/Tortola",
866 "America/Vancouver",
867 "America/Whitehorse",
868 "America/Winnipeg",
869 "America/Yakutat",
870 "Antarctica/Casey",
871 "Antarctica/Davis",
872 "Antarctica/DumontDUrville",
873 "Antarctica/Macquarie",
874 "Antarctica/Mawson",
875 "Antarctica/McMurdo",
876 "Antarctica/Palmer",
877 "Antarctica/Rothera",
878 "Antarctica/Syowa",
879 "Antarctica/Troll",
880 "Antarctica/Vostok",
881 "Arctic/Longyearbyen",
882 "Asia/Aden",
883 "Asia/Almaty",
884 "Asia/Amman",
885 "Asia/Anadyr",
886 "Asia/Aqtau",
887 "Asia/Aqtobe",
888 "Asia/Ashgabat",
889 "Asia/Atyrau",
890 "Asia/Baghdad",
891 "Asia/Bahrain",
892 "Asia/Baku",
893 "Asia/Bangkok",
894 "Asia/Barnaul",
895 "Asia/Beirut",
896 "Asia/Bishkek",
897 "Asia/Brunei",
898 "Asia/Calcutta",
899 "Asia/Chita",
900 "Asia/Colombo",
901 "Asia/Damascus",
902 "Asia/Dhaka",
903 "Asia/Dili",
904 "Asia/Dubai",
905 "Asia/Dushanbe",
906 "Asia/Famagusta",
907 "Asia/Gaza",
908 "Asia/Hebron",
909 "Asia/Hong_Kong",
910 "Asia/Hovd",
911 "Asia/Irkutsk",
912 "Asia/Jakarta",
913 "Asia/Jayapura",
914 "Asia/Jerusalem",
915 "Asia/Kabul",
916 "Asia/Kamchatka",
917 "Asia/Karachi",
918 "Asia/Katmandu",
919 "Asia/Khandyga",
920 "Asia/Krasnoyarsk",
921 "Asia/Kuala_Lumpur",
922 "Asia/Kuching",
923 "Asia/Kuwait",
924 "Asia/Macau",
925 "Asia/Magadan",
926 "Asia/Makassar",
927 "Asia/Manila",
928 "Asia/Muscat",
929 "Asia/Nicosia",
930 "Asia/Novokuznetsk",
931 "Asia/Novosibirsk",
932 "Asia/Omsk",
933 "Asia/Oral",
934 "Asia/Phnom_Penh",
935 "Asia/Pontianak",
936 "Asia/Pyongyang",
937 "Asia/Qatar",
938 "Asia/Qostanay",
939 "Asia/Qyzylorda",
940 "Asia/Rangoon",
941 "Asia/Riyadh",
942 "Asia/Saigon",
943 "Asia/Sakhalin",
944 "Asia/Samarkand",
945 "Asia/Seoul",
946 "Asia/Shanghai",
947 "Asia/Singapore",
948 "Asia/Srednekolymsk",
949 "Asia/Taipei",
950 "Asia/Tashkent",
951 "Asia/Tbilisi",
952 "Asia/Tehran",
953 "Asia/Thimphu",
954 "Asia/Tokyo",
955 "Asia/Tomsk",
956 "Asia/Ulaanbaatar",
957 "Asia/Urumqi",
958 "Asia/Ust-Nera",
959 "Asia/Vientiane",
960 "Asia/Vladivostok",
961 "Asia/Yakutsk",
962 "Asia/Yekaterinburg",
963 "Asia/Yerevan",
964 "Atlantic/Azores",
965 "Atlantic/Bermuda",
966 "Atlantic/Canary",
967 "Atlantic/Cape_Verde",
968 "Atlantic/Faeroe",
969 "Atlantic/Madeira",
970 "Atlantic/Reykjavik",
971 "Atlantic/South_Georgia",
972 "Atlantic/St_Helena",
973 "Atlantic/Stanley",
974 "Australia/Adelaide",
975 "Australia/Brisbane",
976 "Australia/Broken_Hill",
977 "Australia/Darwin",
978 "Australia/Eucla",
979 "Australia/Hobart",
980 "Australia/Lindeman",
981 "Australia/Lord_Howe",
982 "Australia/Melbourne",
983 "Australia/Perth",
984 "Australia/Sydney",
985 "Europe/Amsterdam",
986 "Europe/Andorra",
987 "Europe/Astrakhan",
988 "Europe/Athens",
989 "Europe/Belgrade",
990 "Europe/Berlin",
991 "Europe/Bratislava",
992 "Europe/Brussels",
993 "Europe/Bucharest",
994 "Europe/Budapest",
995 "Europe/Busingen",
996 "Europe/Chisinau",
997 "Europe/Copenhagen",
998 "Europe/Dublin",
999 "Europe/Gibraltar",
1000 "Europe/Guernsey",
1001 "Europe/Helsinki",
1002 "Europe/Isle_of_Man",
1003 "Europe/Istanbul",
1004 "Europe/Jersey",
1005 "Europe/Kaliningrad",
1006 "Europe/Kiev",
1007 "Europe/Kirov",
1008 "Europe/Lisbon",
1009 "Europe/Ljubljana",
1010 "Europe/London",
1011 "Europe/Luxembourg",
1012 "Europe/Madrid",
1013 "Europe/Malta",
1014 "Europe/Mariehamn",
1015 "Europe/Minsk",
1016 "Europe/Monaco",
1017 "Europe/Moscow",
1018 "Europe/Oslo",
1019 "Europe/Paris",
1020 "Europe/Podgorica",
1021 "Europe/Prague",
1022 "Europe/Riga",
1023 "Europe/Rome",
1024 "Europe/Samara",
1025 "Europe/San_Marino",
1026 "Europe/Sarajevo",
1027 "Europe/Saratov",
1028 "Europe/Simferopol",
1029 "Europe/Skopje",
1030 "Europe/Sofia",
1031 "Europe/Stockholm",
1032 "Europe/Tallinn",
1033 "Europe/Tirane",
1034 "Europe/Ulyanovsk",
1035 "Europe/Vaduz",
1036 "Europe/Vatican",
1037 "Europe/Vienna",
1038 "Europe/Vilnius",
1039 "Europe/Volgograd",
1040 "Europe/Warsaw",
1041 "Europe/Zagreb",
1042 "Europe/Zurich",
1043 "Indian/Antananarivo",
1044 "Indian/Chagos",
1045 "Indian/Christmas",
1046 "Indian/Cocos",
1047 "Indian/Comoro",
1048 "Indian/Kerguelen",
1049 "Indian/Mahe",
1050 "Indian/Maldives",
1051 "Indian/Mauritius",
1052 "Indian/Mayotte",
1053 "Indian/Reunion",
1054 "Pacific/Apia",
1055 "Pacific/Auckland",
1056 "Pacific/Bougainville",
1057 "Pacific/Chatham",
1058 "Pacific/Easter",
1059 "Pacific/Efate",
1060 "Pacific/Enderbury",
1061 "Pacific/Fakaofo",
1062 "Pacific/Fiji",
1063 "Pacific/Funafuti",
1064 "Pacific/Galapagos",
1065 "Pacific/Gambier",
1066 "Pacific/Guadalcanal",
1067 "Pacific/Guam",
1068 "Pacific/Honolulu",
1069 "Pacific/Kiritimati",
1070 "Pacific/Kosrae",
1071 "Pacific/Kwajalein",
1072 "Pacific/Majuro",
1073 "Pacific/Marquesas",
1074 "Pacific/Midway",
1075 "Pacific/Nauru",
1076 "Pacific/Niue",
1077 "Pacific/Norfolk",
1078 "Pacific/Noumea",
1079 "Pacific/Pago_Pago",
1080 "Pacific/Palau",
1081 "Pacific/Pitcairn",
1082 "Pacific/Ponape",
1083 "Pacific/Port_Moresby",
1084 "Pacific/Rarotonga",
1085 "Pacific/Saipan",
1086 "Pacific/Tahiti",
1087 "Pacific/Tarawa",
1088 "Pacific/Tongatapu",
1089 "Pacific/Truk",
1090 "Pacific/Wake",
1091 "Pacific/Wallis",
1092];
1093
1094pub fn time_zone_options(selected: &str) -> String {
1109 let mut out = String::with_capacity(TIME_ZONES.len() * 32);
1110 for zone in TIME_ZONES {
1111 let mark = if *zone == selected { " selected" } else { "" };
1113 write!(out, "<option{mark}>{zone}</option>").expect("writing to a String");
1114 }
1115 out
1116}
1117
1118#[cfg(test)]
1119#[path = "../tests/helpers.rs"]
1120mod tests;