Dates
Sept 29, 2020 7:24:21 GMT -8
Post by Uncle Buddy on Sept 29, 2020 7:24:21 GMT -8
# dates
import os
import files
import tkinter as tk
from tkinter import ttk
import sqlite3
import styles as st
import widgets as wdg
import right_click_menu as rcm
import files
'''
Validate dates so bad dates can't be entered. Provides very
flexible input formats and user-selected output formats.
Only non-ambiguous output formats are available.
'''
current_file = files.get_current_file()[0]
date_pref_combos = {}
ST = st.ThemeStyles()
OK_SEPTORS = (' ', '-', '/', '*', '.')
OK_MONTHS = (
'ja', 'f', 'mar', 'ap', 'may', 'jun',
'jul', 'au', 's', 'oc', 'no', 'd')
MONTH_ABBS = (
'ja.', 'jan.', 'f.', 'fe.', 'feb.', 'mar.', 'ap.', 'apr.',
'jun.', 'jul.', 'au.', 'aug.', 's.', 'se.', 'sep.', 'sept.',
'oc.', 'oct.', 'no.', 'nov.', 'd.', 'de.', 'dec.')
INPUT_PFX = ['est', 'cal', 'abt', 'bef', 'aft']
INPUT_SFX = ['ad', 'bc', 'os', 'ns', 'ce', 'bce']
OK_ABBS = INPUT_PFX + INPUT_SFX
MONTH_CONVERSIONS = {
'01': ['01', 'Jan', 'Jan.', 'January'],
'02': ['02', 'Feb', 'Feb.', 'February'],
'03': ['03', 'Mar', 'Mar.', 'March'],
'04': ['04', 'Apr', 'Apr.', 'April'],
'05': ['05', 'May', 'May', 'May'],
'06': ['06', 'June', 'June', 'June'],
'07': ['07', 'July', 'July', 'July'],
'08': ['08', 'Aug', 'Aug.', 'August'],
'09': ['09', 'Sep', 'Sep.', 'September'],
'10': ['10', 'Oct', 'Oct.', 'October'],
'11': ['11', 'Nov', 'Nov.', 'November'],
'12': ['12', 'Dec', 'Dec.', 'December']}
# date output options
EST = ("est", "est.", "estimated", "est'd")
ABT = ("abt", "about", "circa", "ca", "ca.", "approximately", "approx.")
CAL = ("cal", "calc", "calc.", "cal.", "calculated", "calc'd")
BEF = ("bef", "bef.", "prior to", "before")
AFT = ("aft", "aft.", "later than", "after")
BC = ("BCE", "BC", "B.C.E.", "B.C.")
AD = ("CE", "AD", "C.E.", "A.D.")
JULIAN = ("OS", "O.S.", "old style", "Old Style")
GREGORIAN = ("NS", "N.S.", "new style", "New Style")
PAIRS = ((BEF, AFT), (BC, AD), (JULIAN, GREGORIAN))
ABB_PAIRS = []
q = 0
for pair in PAIRS:
paired = []
for r, s in zip(pair[0], pair[1]):
stg = '{}/{}'.format(r, s)
paired.append(stg)
ABB_PAIRS.append(paired)
q += 1
DATE_PREF_COMBOS = (
("18 April 1906", "18 Apr 1906", "18 Apr. 1906", "April 18, 1906",
"Apr 18, 1906", "Apr. 18, 1906", "1906-04-18", "1906/04/18",
"1906.04.18"),
EST, ABT, CAL,
ABB_PAIRS[0], ABB_PAIRS[1], ABB_PAIRS[2],
("from [date 1] to [date 2]", "fr. [date 1] to [date 2]",
"frm [date 1] to [date 2]", "fr [date 1] to [date 2]"),
("btwn [date 1] & [date 2]", "btwn [date 1] and [date 2]",
"bet [date 1] & [date 2]", "bet [date 1] and [date 2]",
"bet. [date 1] & [date 2]" , "bet. [date 1] and [date 2]",
"between [date 1] & [date 2]", "between [date 1] and [date 2]"))
DATE_FORMATS = (
'alpha_dmy', 'alpha_dmy_abb', 'alpha_dmy_dot', 'alpha_mdy',
'alpha_mdy_abb', 'alpha_mdy_dot', 'iso_dash', 'iso_slash', 'iso_dot')
SPAN_FORMATS = ("from_to", "fr._to", "frm_to", "fr_to")
RANGE_FORMATS = (
"btwn_&", "btwn_and", "bet_&", "bet_and", "bet._&",
"bet._and", "between_&", "between_and")
DATE_FORMAT_LOOKUP = dict(zip(DATE_PREF_COMBOS[0], DATE_FORMATS))
SPAN_FORMAT_LOOKUP = dict(zip(DATE_PREF_COMBOS[7], SPAN_FORMATS))
RANGE_FORMAT_LOOKUP = dict(zip(DATE_PREF_COMBOS[8], RANGE_FORMATS))
OK_PREFIXES = ABT+EST+CAL+BEF+AFT
OK_SUFFIXES = BC+AD+JULIAN+GREGORIAN
class ValidDate():
'''
All dates are single dates till combined for storage or display.
The 7 categories of date are:
simple (18 April 1856)
span1_known (1855 in '1855 to 1859')
span1_unknown (? in '? to 1859')
span2_known (1859 in '1855 to 1859')
span2_unknown (? in '1855 to ?')
range1 (1900 in 'btwn 1900 and 1910')
range2 (1910 in 'btwn 1900 and 1910')
'''
def __init__(self):
self.widg = ''
self.year = ''
self.month = ''
self.day = ''
self.pref = ''
self.suff = ''
self.prefix = ''
self.suffix = ''
self.code = ''
self.small = []
self.med = []
self.big = []
self.numbers = []
self.output_1 = '' # formatted date
self.output_2 = '' # formatted date
self.date_1 = '' # iso date saved for end of 2-date process
self.link = []
self.date_to_store = '' # single final iso or first iso for later
self.date_format = ''
self.est_format = ''
self.abt_format = ''
self.cal_format = ''
self.befaft_format = ''
self.epoch_format = ''
self.julegreg_format = ''
self.input_type = ''
self.and_to = None
self.input_1 = [] # raw input
self.input_2 = [] # raw input
self.iso_date = '' # standardized single iso date
self.valid = True
self.ents = []
self.cols = []
self.valids = []
self.clarifier_is_running = False
self.unknowns = []
self.ok = None
self.frm = None
self.finding_id = None
# UTILITIES
def reset_all_vars(self):
self.reset_one_use_vars()
self.and_to = None
self.output_1 = ''
self.output_2 = ''
self.date_1 = ''
self.link = []
self.input_1 = []
self.input_2 = []
self.widg = ''
self.date_to_store = ''
self.finding_id = None
self.clarifier_is_running = False
def reset_one_use_vars(self):
self.year = ''
self.month = ''
self.day = ''
self.pref = ''
self.suff = ''
self.prefix = ''
self.suffix = ''
self.code = ''
self.small = []
self.med = []
self.big = []
self.numbers = []
self.iso_date = ''
self.valid = True
self.ents = []
self.cols = []
self.valids = []
self.unknowns = []
self.ok = None
self.frm = None
def store_valid_date(self):
'''
finding_id is sent over from FindingsTable class where
it's used to identify a row in the findings table. Gets validated
date from edited findings table date fields on FocusOut,
and stores it in ISO format in the right row of the finding table
in database.
'''
current_file = files.get_current_file()[0]
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute('''
UPDATE finding
SET date = ?
WHERE finding_id = ?''',
(self.date_to_store, self.finding_id))
conn.commit()
cur.close()
conn.close()
def strip_lead_trail(self, stg, sym):
stg = stg.strip(sym)
return stg
def fix_double_spaces(self, stg, delim):
''' e.g. " 1985 05 12-* " becomes "1985 05 12" '''
# run loop more than once in case user inputs various septors
for sym in delim:
stg = self.strip_lead_trail(stg, sym)
for sym in delim:
stg = self.strip_lead_trail(stg, sym)
for sym in delim:
stg = self.strip_lead_trail(stg, sym)
for sym in delim:
if sym*2 in stg:
stg = stg.replace(sym*2, sym)
stg = self.fix_double_spaces(stg, delim)
return stg
def minimize_mo_stg(self, mo_stg):
if mo_stg.startswith(('j', 'm')):
month = mo_stg[0:3]
elif mo_stg.startswith(('a', 'o', 'n')):
month = mo_stg[0:2]
else:
month = mo_stg[0]
if month == 'jan':
month = 'ja'
return month
def change_month_to_num(self, mo_stg):
ISO_MONTHS = {
'ja': '01', 'f': '02', 'mar': '03', 'ap': '04', 'may': '05',
'jun': '06', 'jul': '07', 'au': '08', 's': '09', 'oc': '10',
'no': '11', 'd': '12'}
for k,v in ISO_MONTHS.items():
if mo_stg.lower().startswith(k) is True:
month = v
return month
def validate_month_by_length(self):
if self.month is None:
self.day = '00'
return '00'
elif int(self.month) in (4, 6, 9, 11):
if len(self.day) == 0:
self.day = '00'
if int(self.day) > 30:
self.widg.clear_bad_data(
'int(self.day) > 30', 'That month has only 30 days.')
self.reset_all_vars()
return
elif int(self.month) == 2:
leap = False
if int(self.year)%4 == 0:
if int(self.year)%100 == 0:
if int(self.year)%400 == 0:
leap = True
elif int(self.year)%100 != 0:
leap = True
if len(self.day) == 0:
self.month = '2'
elif leap is False:
if int(self.day) > 28:
self.widg.clear_bad_data(
'int(self.day) > 28', 'February in a non-leap-year '
'has only 28 days.')
self.reset_all_vars()
return
else:
self.month = '2'
elif leap is True:
if int(self.day) > 29:
self.widg.clear_bad_data(
'int(self.day) > 29', 'February in a leap year '
'has only 29 days.')
self.reset_all_vars()
return
else:
self.month = '2'
return self.month
def standardize_for_iso(self):
if len(self.year) < 4:
zeroes = 4 - len(self.year)
self.year = '0' * zeroes + self.year
if self.month is not None and len(self.month) < 2:
zeroes = 2 - len(self.month)
self.month = '0' * zeroes + self.month
if self.day is not None and len(self.day) < 2:
zeroes = 2 - len(self.day)
self.day = '0' * zeroes + self.day
if len(self.suff) > 0:
self.suff = self.suff
date = '{}-{}-{}-{}-{}'.format(
self.pref, self.year, self.month, self.day, self.suff)
if self.input_type in ('range1', 'span1'):
self.date_1 = date
return date
def handle_commas(self, input, day, septor, comma_count):
if comma_count == 1:
idx_comma = input.find(',')
idx_septor = input.find(septor)
idx_last = len(input) - 1
idx_septor_rt = input.rfind(septor)
if idx_septor < idx_comma and idx_comma != idx_last:
day = input[idx_septor+1:idx_comma]
elif idx_septor < idx_comma and idx_comma == idx_last:
day = input[idx_septor_rt + 1:idx_last]
if len(day) > 2 or int(day) > 31:
self.widg.clear_bad_data(
'len(day) > 2 or int(day) > 31',
'Day over 31 or too many digits.')
self.reset_all_vars()
return
elif idx_septor > idx_comma:
day = input[0:idx_comma]
input = input.replace(',', '')
return input
# HANDLE RAW INPUT
def validate_date_or_not(self, input, widg, finding_id=None):
'''
When clarifier dialog opens, it sets a variable to True
so that the FocusOut of the events table cell will
not trigger another validation process.
'''
if self.clarifier_is_running is True:
self.clarifier_is_running = False
return
elif self.clarifier_is_running is False:
self.classify_one_date(widg, finding_id=finding_id)
def classify_one_date(self, widg, finding_id=None):
'''
Gets raw input, figures out what kind of input
it contains, and runs the right function for
that type of input. If two dates are in the input,
the first date in the compound date is run first
but both dates are classfied immediately upon
FocusOut. The input_type value refers to the first
date till its validation process is complete, then
it is set to the value and input_type of the second
date in the compound input.
Runs on FocusOut of any date input widget
if widget contents have changed or when constructing
findings table from stored data. If date comes from db it's
already validated and no validation is done. finding_id
is not used in this function but is set here because it's
passed in from self.validate_date_or_not() being called
in the FindingsTable class where it says v(input, widg,
finding_id=self.finding_id) in use_input(). self.finding_id
is set by FocusIn or Button-1 click in EntryLabel using
get_cell_ref(). If input
is typed into findings table cell by user, input has to be
validated and finding_id is needed. If user input is
typed into a tester field in the date prefs tab, validation
is done but no finding_id is needed. If user clicks in a
field that displays an existing date,
but tabs out w/out making changes,
no validation is done. self.finding_id is not None when
input is from user editing date on findings table;
self.finding_id is not None when input is making table
from values stored in db; self.finding_id is None when
input is from tester date fields in prefs. '''
self.widg = widg
self.finding_id = finding_id
input = self.widg.cget('text')
if len(input) == 0:
self.date_to_store = '-0000-00-00-'
self.store_valid_date()
return
if input.count('?') > 1:
self.widg.clear_bad_data(
"input.count('?') > 1",
"Only one unknown date allowed in span.")
self.reset_all_vars()
return
if ' to ' in input:
self.and_to = ' to '
elif ' and ' in input:
self.and_to = ' and '
else:
self.input_type = 'single'
self.input_1 = input
if self.and_to:
lst = input.split(self.and_to)
self.input_1 = [lst[0]]
self.input_2 = [lst[1]]
if self.and_to == ' to ' and '?' in input:
d = 0
for lst in [self.input_1, self.input_2]:
if lst[0].strip() == '?' and d == 0:
self.input_type = 'span1_unknown'
self.input_2.append('span2')
elif lst[0].strip() == '?' and d == 1:
self.input_type = 'span1'
self.input_1 = self.input_1[0]
lst.append('span2_unknown')
d += 1
elif self.and_to == ' to ':
self.input_type = 'span1'
self.input_1 = self.input_1[0]
self.input_2.append('span2')
elif self.and_to == ' and ':
self.input_type = 'range1'
self.input_1 = self.input_1[0]
self.input_2.append('range2')
if self.input_type in ('single', 'span1', 'range1'):
self.prepare_date(self.input_1)
elif self.input_type == 'span1_unknown':
self.output_unknown_span1()
# METHODS FOR DATES TREATED IN ISOLATION FROM OTHER DATES
def prepare_date(self, input):
input = self.fix_double_spaces(input, OK_SEPTORS)
for abbrev in MONTH_ABBS:
if input.find(abbrev.lower()) != -1:
new_abbrev = abbrev.strip('.')
input = input.replace(abbrev, new_abbrev)
septors = []
for char in input:
if char in OK_SEPTORS and char not in septors:
septors.append(char)
day = '00'
comma_count = input.count(',')
if comma_count > 1:
self.widg.clear_bad_data(
"comma_count > 1",
"Too many commas in date input.")
self.reset_all_vars()
return
if len(septors) == 0:
input = [input]
elif len(septors) == 1:
septor = septors[0]
if ',' in input:
input = self.handle_commas(input, day, septor, comma_count)
if input:
input = input.split(septor)
elif input is None:
return
else:
self.widg.clear_bad_data(
"len(septors) != 1 or 0",
"No more than one character out of [space, dot, "
"forward slash, dot, or star] can be used as a "
"separator between date parts. Exception: "
"only a space can be used as a spacer around "
"'and' or 'to'. HINT: Just separate all date "
"parts with a space.")
self.reset_all_vars()
return
for char in input:
if char.isalnum() is False and char not in OK_SEPTORS:
self.widg.clear_bad_data(
'non-alphanum char not in OK_SEPTORS',
'That character is not allowed in a date input.')
self.reset_all_vars()
return
self.sift_input(input, day=day)
def sift_input(self, input, day='00'):
words = []
prefixes = []
suffixes = []
months = []
mo_stg = ''
a = '00'
c = None
d = None
if len(input) > 5:
self.widg.clear_bad_data(
'len(input) > 5',
'A date can have only five parts including '
'prefix and suffix.')
self.reset_all_vars()
return
for part in input:
if part.isalpha() is True:
words.append(part)
for word in words:
if words.count(word) > 1:
self.widg.clear_bad_data(
'words.count(word) > 1',
'A word can\'t be repeated within a single date.')
self.reset_all_vars()
return
word = word.lower()
if word in OK_ABBS and word in OK_PREFIXES:
prefixes.append(word)
elif word in OK_ABBS and word.upper() in OK_SUFFIXES:
suffixes.append(word)
elif word.startswith(OK_MONTHS) is True:
months.append(word)
else:
month = ''
self.widg.clear_bad_data(
'bad month string',
'Misc. date input error, error type unknown.')
self.reset_all_vars()
return
if len(prefixes) > 1 or len(suffixes) > 1 or len(months) > 1:
self.widg.clear_bad_data(
'len(prefixes) > 1 or len(suffixes) > 1 or '
'len(months) > 1',
'More than one prefix, suffix, or month was input.')
self.reset_all_vars()
return
else:
if len(prefixes) == 1:
self.pref = prefixes[0]
if len(suffixes) == 1:
self.suff = suffixes[0].upper()
if len(months) == 1:
mo_stg = months[0]
mo_stg = self.minimize_mo_stg(mo_stg)
a = self.change_month_to_num(mo_stg)
else:
month = '00'
for part in input:
if part.isnumeric() is True:
if len(part) == 1:
part = '0{}'.format(part)
self.numbers.append(part)
if len(self.numbers) > 3:
self.widg.clear_bad_data(
'len(self.numbers) > 3',
'More than three numbers were input.')
self.reset_all_vars()
return
if len(day) == 1:
day = '0{}'.format(day)
same = False
for num in self.numbers:
if int(num) > 9999 or len(num) > 5:
self.widg.clear_bad_data(
'int(num) > 9999 or len(num) > 5',
'Maximum year is 9999.')
self.reset_all_vars()
return
if (5 > len(num) > 2) or (31 < int(num) < 10000):
self.big.append(num)
elif 0 < len(num) < 3:
# code c is btwn 1 & 12 known to be a day
# because it follows a comma
# code d ditto btwn 13 & 31
if 0 < int(num) < 13:
if num != day:
self.small.append(num)
elif num == day and same is False:
c = day
same = True
elif num == day and same is True:
self.small.append(num)
elif 12 < int(num) < 32:
if num != day:
self.med.append(num)
elif num == day:
d = day
elif int(num) == 0:
pass
else:
self.widg.clear_bad_data(
'bad number input',
'Misc. date input error of unknown type.')
self.reset_all_vars()
return
sm = len(self.small)
self.code = self.code + 's' * sm
md = len(self.med)
self.code = self.code + 'm' * md
bg = len(self.big)
self.code = self.code + 'b' * bg
if a != '00':
self.code = self.code + 'a'
if day != '00':
if c is not None:
self.code = self.code + 'c'
self.grok_codes(month=a, day=c)
elif d is not None:
self.code = self.code + 'd'
self.grok_codes(month=a, day=d)
else:
self.grok_codes(month=a)
def grok_codes(self, month='00', day='00'):
indented_bullet = '\n \u2022 '.format()
if ('bb' in self.code or 'cc' in self.code or
'aa' in self.code or 'dd' in self.code or
self.code in (
'mmm', 'mmb', 'mmc', 'mbc', 'a',
'c', 'd', 'mm', 'mb', '')):
self.widg.clear_bad_data(
'disallowed date code',
'Not a valid date for one of these reasons:'
'{}no input for month;{}no input for year;'
'{}too many numbers over 31;{}more than one month '
'given;{}more than one day given.'.format(
indented_bullet, indented_bullet, indented_bullet,
indented_bullet, indented_bullet))
self.reset_all_vars()
return
if self.code in ('smm',):
self.month = self.small[0]
self.numbers = sorted(self.numbers)
x = self.numbers[1]
if self.numbers[2] == x:
self.year = x
self.day = x
else:
self.make_iso()
return
elif self.code in ('smd', 'smc'):
self.month = self.small[0]
self.year = self.med[0]
self.day = day
elif self.code in ('ssb',):
self.numbers = sorted(self.numbers, reverse=True)
x = self.numbers[1]
if self.numbers[2] == x:
self.year = self.numbers[0]
self.month = x
self.day = x
else:
self.year = self.big[0]
self.make_iso()
return
elif self.code in ('sbc', 'sbd'):
self.month = self.small[0]
self.year = self.big[0]
self.day = day
elif self.code in ('sba',):
self.year = self.big[0]
self.month = month
self.day = self.small[0]
elif self.code in ('bac', 'bad'):
self.year = self.big[0]
self.month = month
self.day = day
elif self.code in ('smb', 'mba'):
self.year = self.big[0]
self.day = self.med[0]
if 's' in self.code:
self.month = self.small[0]
else:
self.month = month
elif self.code in ('sss',):
x = self.numbers[0]
if self.numbers[1] == x and self.numbers[2] == x:
self.year = x
self.month = x
self.day = x
else:
self.make_iso()
return
elif self.code in ('ssc',):
self.day = day
x = self.numbers[0]
if self.numbers[1] == x and self.numbers[2] == x:
self.year = x
self.month = x
else:
self.make_iso()
return
elif self.code in ('ssm',):
self.numbers = sorted(self.numbers)
month = self.numbers[0]
if self.numbers[1] == month:
self.month = month
self.make_iso()
return
else:
self.make_iso()
return
elif self.code in ('ssd',):
self.day = day
self.numbers = sorted(self.numbers)
x = self.numbers[0]
if self.numbers[1] == x:
self.year = x
self.month = x
else:
self.make_iso()
return
elif self.code in ('ssa',):
self.month = month
x = int(month)
if int(self.numbers[0]) == x and int(self.numbers[1]) == x:
yr = str(x)
mo = str(x)
dy = str(x)
self.add_zeroes(yr, mo, dy)
elif (int(self.numbers[0]) != x and
int(self.numbers[0]) == int(self.numbers[1])):
yr = str(self.numbers[0])
mo = str(x)
dy = yr
self.add_zeroes(yr, mo, dy)
else:
self.month = month
self.make_iso()
return
elif self.code in ('sac', 'sad'):
self.year = self.small[0]
self.month = month
self.day = day
elif self.code in ('sma', 'mma'):
self.month = month
self.make_iso()
return
elif self.code in ('mac', 'mad'):
self.year = self.med[0]
self.month = month
self.day = day
elif len(self.code) < 3:
if 'c' in self.code or 'd' in self.code:
self.widg.clear_bad_data(
"len(self.code) < 3 and 'c' or 'd' in self.code",
"If date has only two parts, one of them can't "
"be a day. (Treebard interprets any number "
"following a comma to be a day.)")
self.reset_all_vars()
return
day = '00'
if self.code in ('sa', 'ma'):
self.month = month
if self.code[0] == 's':
self.year = self.small[0]
elif self.code [0] == 'm':
self.year = self.med[0]
elif self.code in ('ba',):
self.year = self.big[0]
self.month = month
elif self.code in ('ss',):
x = self.numbers[0]
if self.numbers[1] == x:
self.year = x
self.month = x
else:
self.day = day
self.make_iso()
return
elif self.code in ('sm',):
self.year = self.med[0]
self.month = self.small[0]
elif self.code in ('sb',):
self.year = self.big[0]
self.month = self.small[0]
elif self.code in ('s', 'm', 'b'):
self.month = None
self.day = None
if self.code in ('s',):
self.year = self.small[0]
elif self.code in ('m',):
self.year = self.med[0]
elif self.code in ('b',):
self.year = self.big[0]
self.month = self.validate_month_by_length()
if self.month:
self.iso_date = self.standardize_for_iso()
if self.input_type in ('single', 'range1', 'span1'):
self.output_first_date()
elif self.input_type in ('range2', 'span2'):
self.output_second_date()
elif self.input_type == 'span2_unknown':
self.output_unknown_span2()
def format_one_date(self):
self.update_display_prefs()
date = self.apply_display_prefs()
self.widg.config(text=date)
self.reset_all_vars()
# METHODS FOR MEMBERS OF A PAIR OF DATES COMPRISING A COMPOUND DATE
def output_unknown_span1(self):
self.output_1 = '?'
self.input_type = self.input_2[1]
self.input_2 = self.input_2[0]
self.reset_one_use_vars()
self.prepare_date(self.input_2)
def output_unknown_span2(self):
span_format = self.link[0].split('_')
ordered_compound_date = '{} {} {} ?'.format(
span_format[0], self.output_1, span_format[1])
self.widg.config(state='normal')
self.widg.config(text=ordered_compound_date)
self.widg.config(state='readonly', width=len(self.widg.get()))
self.date_to_store = '{}-to-?'.format(self.date_1)
if self.finding_id:
self.store_valid_date()
self.reset_all_vars()
return ordered_compound_date
def output_first_date(self):
date = self.iso_date.split('-')
self.prefix = date[0]
self.year = date[1]
self.month = date[2]
self.day = date[3]
self.suffix = date[4]
self.date_to_store = '{}-{}-{}-{}-{}'.format(
self.prefix, self.year, self.month,
self.day, self.suffix)
if self.input_type == 'single':
if self.finding_id:
self.store_valid_date()
self.format_one_date()
elif self.input_type in ('span1', 'range1'):
self.format_first_date()
def format_first_date(self):
self.update_display_prefs()
date = self.apply_display_prefs()
self.output_1 = date
self.input_type = self.input_2[1]
self.input_2 = self.input_2[0]
self.reset_one_use_vars()
if self.input_type in ('span2', 'range2'):
self.prepare_date(self.input_2)
elif self.input_type == 'span2_unknown':
self.output_unknown_span2()
def output_second_date(self):
if self.output_1 == '?':
self.date_1 == '?'
date = self.iso_date.split('-')
self.prefix = date[0]
self.year = date[1]
self.month = date[2]
self.day = date[3]
self.suffix = date[4]
self.date_to_store = '{}-{}-{}-{}-{}-{}-{}'.format(
self.date_1, self.and_to.strip(),
self.prefix, self.year, self.month,
self.day, self.suffix)
self.format_second_date()
def format_second_date(self):
self.update_display_prefs()
date = self.apply_display_prefs()
self.output_2 = date
if self.output_1 == '?':
span_format = self.link[0].split('_')
link = self.and_to.replace(' ', '-')
self.date_to_store = '?-to-{}'.format(self.iso_date)
if self.finding_id:
self.store_valid_date()
ordered_compound_date = '{} ? {} {}'.format(
span_format[0], span_format[1], self.output_2)
else:
both_ymd = []
for stg in (self.date_1, self.iso_date):
stg = stg.strip('-')
both_ymd.append(stg.split('-'))
ordered_compound_date = self.sort_by_era(both_ymd)
self.widg.config(text=ordered_compound_date)
self.reset_all_vars()
def sort_by_era(self, both_ymd):
sorter_by_epoch = []
e = 0
for lst in both_ymd:
last_stg = len(lst) - 1
sfix = ''
if (lst[last_stg].isalpha() is True and
lst[last_stg] not in ('ns', 'os')):
sfix = lst[last_stg]
mo = 0
dy = 0
if lst[0].isalpha() is True:
f = 1
elif lst[0].isalpha() is False:
f = 0
yr = int(lst[f])
if lst[f+1] != '00':
mo = int(lst[f+1])
if lst[f+2] != '00':
dy = int(lst[f+2])
int_date = (yr, mo, dy)
if e == 0:
sorter_by_epoch.append(
[int_date, self.output_1, sfix, self.date_1])
elif e == 1:
sorter_by_epoch.append(
[int_date, self.output_2, sfix, self.iso_date])
e += 1
one_suffix = sorter_by_epoch[0][2]
other_suffix = sorter_by_epoch[1][2]
if one_suffix in ('', 'ad') and other_suffix in ('', 'ad'):
dates = sorted(sorter_by_epoch)
elif one_suffix == 'bc' and other_suffix == one_suffix:
dates = sorted(sorter_by_epoch, reverse=True)
elif other_suffix == 'bc' and one_suffix in ('', 'ad'):
dates = [sorter_by_epoch[1], sorter_by_epoch[0]]
elif other_suffix in ('', 'ad') and one_suffix == 'bc':
dates = [sorter_by_epoch[0], sorter_by_epoch[1]]
first_date = dates[0][1]
second_date = dates[1][1]
span_format = self.link[0].split('_')
range_format = self.link[1].split('_')
if self.and_to == ' to ':
ordered_compound_date = '{} {} {} {}'.format(
span_format[0], first_date, span_format[1], second_date)
elif self.and_to == ' and ':
ordered_compound_date = '{} {} {} {}'.format(
range_format[0],
first_date,
range_format[1],
second_date)
iso_1 = dates[0][3]
iso_2 = dates[1][3]
link = self.and_to.replace(' ', '-')
self.date_to_store = '{}{}{}'.format(
iso_1, link, iso_2)
if self.finding_id:
self.store_valid_date()
return ordered_compound_date
# APPLY USER FORMAT DISPLAY PREFERENCES
def format_unknown_date_for_table(self, parts):
self.update_display_prefs()
if parts[6] == '?':
self.prefix = parts[0]
self.year = parts[1]
self.month = parts[2]
self.day = parts[3]
self.suffix = parts[4]
date1 = self.apply_display_prefs()
span_format = self.link[0].split('_')
ordered_compound_date = '{} {} {} ?'.format(
span_format[0], date1, span_format[1])
self.reset_all_vars()
elif parts[0] == '?':
self.prefix = parts[2]
self.year = parts[3]
self.month = parts[4]
self.day = parts[5]
self.suffix = parts[6]
date2 = self.apply_display_prefs()
span_format = self.link[0].split('_')
ordered_compound_date = '{} ? {} {}'.format(
span_format[0], span_format[1], date2)
self.reset_all_vars()
return ordered_compound_date
def select_function(self, value, widg=None):
self.widg = widg
parts = value.split('-')
if parts[6] == '?' or parts[0] == '?':
return self.format_unknown_date_for_table(parts)
else:
return self.format_compound_date_for_table(value)
def format_compound_date_for_table(self, value):
parts = value.split('-')
self.update_display_prefs()
self.prefix = parts[0]
self.year = parts[1]
self.month = parts[2]
self.day = parts[3]
self.suffix = parts[4]
date1 = self.apply_display_prefs()
self.and_to = ' {} '.format(parts[5])
self.prefix = parts[6]
self.year = parts[7]
self.month = parts[8]
self.day = parts[9]
self.suffix = parts[10]
date2 = self.apply_display_prefs()
self.output_1 = date1
self.output_2 = date2
two_iso = value.split(self.and_to.strip())
both_ymd = []
for stg in two_iso:
stg = stg.strip('-')
both_ymd.append(stg.split('-'))
ordered_compound_date = self.sort_by_era(both_ymd)
self.reset_all_vars()
return ordered_compound_date
def format_date_for_table(self, value, widg=None):
''' Formats single dates. '''
self.update_display_prefs()
self.widg = widg
parts = value.split('-')
self.prefix = parts[0]
self.year = parts[1]
self.month = parts[2]
self.day = parts[3]
self.suffix = parts[4]
date = self.apply_display_prefs()
self.reset_all_vars()
return date
def update_display_prefs(self):
date_formats_all = self.get_date_output_prefs()
self.date_format = date_formats_all[0]
self.est_format = date_formats_all[1]
self.abt_format = date_formats_all[2]
self.cal_format = date_formats_all[3]
self.befaft_format = date_formats_all[4]
self.epoch_format = date_formats_all[5]
self.julegreg_format = date_formats_all[6]
span_format = date_formats_all[7]
range_format = date_formats_all[8]
if self.and_to:
self.link = self.pass_compound_format(
span_format, range_format)
def add_zeroes(self, yr, mo, dy):
if len(yr) < 4:
self.year = yr.zfill(4)
if mo is not None and len(mo) < 2:
self.month = mo.zfill(2)
if dy is not None and len(dy) < 2:
self.day = dy.zfill(2)
elif dy is not None and len(dy) == 2:
self.day = dy
def apply_display_prefs(self):
if self.prefix in EST:
self.prefix = self.est_format
elif self.prefix in ABT:
self.prefix = self.abt_format
elif self.prefix in CAL:
self.prefix = self.cal_format
elif self.prefix in BEF+AFT:
choices = self.befaft_format.split('/')
if self.prefix == 'bef':
self.prefix = choices[0]
elif self.prefix == 'aft':
self.prefix = choices[1]
if self.suffix.upper() in BC+AD:
choices = self.epoch_format.split('/')
if self.suffix in ('bc', 'bce', 'BC', 'BCE'):
self.suffix = choices[0]
elif self.suffix in ('ad', 'ce', 'AD', 'CE'):
self.suffix = choices[1]
elif len(self.suffix) == 0 and (0 < int(self.year) < 1000):
choices = self.epoch_format.split('/')
self.suffix = choices[1]
elif self.suffix.upper() in JULIAN+GREGORIAN:
choices = self.julegreg_format.split('/')
if self.suffix == 'os':
self.suffix = choices[0]
elif self.suffix == 'ns':
self.suffix = choices[1]
if self.date_format.startswith('alpha') is True:
abc_form = True
iso_form = False
elif self.date_format.startswith('iso') is True:
iso_form = True
abc_form = False
if abc_form is True:
if self.date_format.endswith('abb'):
for k,v in MONTH_CONVERSIONS.items():
if self.month == k:
self.month = v[1]
elif self.date_format.endswith('dot'):
for k,v in MONTH_CONVERSIONS.items():
if self.month == k:
self.month = v[2]
else:
for k,v in MONTH_CONVERSIONS.items():
if self.month == k:
self.month = v[3]
if self.day == ('00'):
self.day = ''
self.year = self.year.lstrip('0')
if self.month == '00':
self.month = ''
if 'dmy' in self.date_format:
date = '{} {} {} {} {}'.format(
self.prefix, self.day, self.month,
self.year, self.suffix)
date = date.replace(' ', ' ').replace(' ', ' ')
elif 'mdy' in self.date_format:
if len(self.month) > 0:
if len(self.day) > 0:
date = '{} {} {}, {} {}'.format(
self.prefix, self.month, self.day,
self.year, self.suffix)
elif len(self.day) == 0:
date = '{} {} {} {}'.format(
self.prefix, self.month, self.year,
self.suffix)
elif len(self.month) == 0 and len(self.day) == 0:
date = '{} {} {}'.format(self.prefix, self.year,
self.suffix)
elif iso_form is True:
if self.date_format.endswith('dash') is True:
septor = '-'
elif self.date_format.endswith('slash') is True:
septor = '/'
elif self.date_format.endswith('dot') is True:
septor = '.'
date = '{} {}{}{}{}{} {}'.format(
self.prefix, self.year, septor, self.month,
septor, self.day, self.suffix)
date = date.strip()
return date
def pass_compound_format(self, h, r):
lst = [h, r]
return lst
def get_date_output_prefs(self):
current_file = files.get_current_file()[0]
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('''
SELECT date_format, est, abt, cal, bef_aft,
bc_ad, os_ns, span, range
FROM date_per_tree
WHERE date_per_tree_id = 1''')
tree_date_format = cur.fetchall()[0]
cur.close()
conn.close()
# conn = sqlite3.connect(st.conn_fig)
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('''
SELECT date_format, est, abt, cal, bef_aft,
bc_ad, os_ns, span, range
FROM date_per_user
WHERE date_per_user_id = 1''')
user_date_format = cur.fetchall()[0]
cur.execute('''
SELECT date_format, est, abt, cal, bef_aft,
bc_ad, os_ns, span, range
FROM date_global
WHERE date_global_id = 1''')
global_date_format = cur.fetchall()[0]
cur.close()
conn.close()
composite_date_prefs = list(global_date_format)
y = 0
for b, c in zip(user_date_format, tree_date_format):
if c is not None:
composite_date_prefs[y] = c
elif b is not None:
composite_date_prefs[y] = b
y += 1
return composite_date_prefs
# DATES CLARIFIER DIALOG
def validate_iso(self):
ymd = []
if len(self.month) == 1:
self.month = '0{}'.format(self.month)
if len(self.day) == 1:
self.day = '0{}'.format(self.day)
if len(self.year) == 1:
self.year = '0{}'.format(self.year)
ymd.append(self.year)
ymd.append(self.month)
ymd.append(self.day)
for num in ymd:
if num not in self.valids:
for child in self.clarifier.winfo_children():
if (child.winfo_class() == 'Frame' and
child.winfo_subclass() == 'Entry'):
child.delete(0, 'end')
self.valid = False
break
return self.valid
def format_for_tester(self):
''' Can this be deleted? '''
print('format_for_tester executed')
def close_iso(self, evt=None):
self.clarifier.destroy()
self.clarifier_is_running = False
def send_date(self, evt=None):
self.valid = True
for tup in self.cols:
input = tup[1].get().strip()
if len(input) == 0 or input.isnumeric() is False:
tup[1].delete(0, 'end')
tup[1].focus_set()
return
if tup[0] == 0:
self.year = input
elif tup[0] == 1:
if 0 < int(input) < 13:
self.month = input
else:
for ent in self.ents:
ent.delete(0, 'end')
tup[1].focus_set()
return
elif tup[0] == 2:
self.day = input
self.month = self.validate_month_by_length()
if self.month:
self.valid = self.validate_iso()
if self.valid is True:
self.iso_date = self.standardize_for_iso()
if self.input_type in ('single', 'range1', 'span1'):
self.output_first_date()
elif self.input_type in ('range2', 'span2'):
self.output_second_date()
self.close_iso()
def clear_typed(self, evt):
''' Runs on every FocusOut from a clarifier entry. '''
used = []
empty_entries = []
last_unused = ''
for child in self.frm.mainframe.winfo_children():
if (child.winfo_class() == 'Frame' and
child.winfo_subclass() == 'Entry'):
content = child.get()
if len(content) == 0:
empty_entries.append(child)
else:
used.append(content)
vestige = list(self.unknowns)
for content in used:
if content in vestige:
vestige.remove(content)
if len(vestige) == 1:
last_unused = vestige[0]
if len(empty_entries) == 1:
empty_entries[0].insert(0, last_unused)
self.ok.focus_set()
def make_header_row(self, heads):
n = 0
for head in heads:
lab = wdg.LabelColumn(
self.frm.mainframe, text=heads[n])
lab.grid(column=n, row=0, sticky='ew', pady=(12,0), padx=12)
n += 1
def make_iso_frame(self):
heads = ('Year', 'Month', 'Day')
self.frm = wdg.Table(self.clarifier, heads)
self.frm.grid(column=0, row=1, columnspan=3)
self.frm.grid_columnconfigure(0, weight=1)
self.frm.grid_columnconfigure(1, weight=1)
self.frm.grid_columnconfigure(2, weight=1)
instrux = wdg.LabelH3(self.clarifier, text='')
instrux.grid(column=0, row=0, columnspan=3, pady=(0,12))
self.valids = list(self.numbers)
if len(self.valids) == 2 and len(self.month) == 0:
self.valids.append('00')
elif len(self.valids) == 2:
self.valids.append(self.month)
self.make_header_row(heads)
knowns = []
h = 0
for name in (self.year, self.month, self.day):
if name is None:
name = 'nothing'
elif len(name) == 0:
name = 'zip'
if name:
if name == 'zip':
ent = wdg.Entry(
self.frm.mainframe,
# width=5,
# font=formats['input_font']
)
ent.grid(column=h, row=2, padx=12, pady=12)
self.ents.append(ent)
self.cols.append((ent.grid_info()['column'], ent))
ent.bind('<FocusOut>', self.clear_typed)
ent.config(width=5)
else:
lab = wdg.Label(self.frm.mainframe, text=name)
lab.grid(column=h, row=2, padx=(3,0), pady=12, sticky='we')
knowns.append(name)
h += 1
self.ents[0].focus_set()
self.unknowns = list(self.valids)
for num in knowns:
if num in self.unknowns:
self.unknowns.remove(num)
for num in self.unknowns:
idx = self.unknowns.index(num)
num = num.lstrip('0')
self.unknowns[idx] = num
definables = ', '.join(self.unknowns)
instrux.config(text='Define {}:'.format(definables))
def make_iso(self):
'''
Open a clarifier dialog so user can input ISO
format of intended date. Only opens for ambiguous date
input such as "10 9 1717" which could be Oct 9 or Sept 10.
Autofills as much as possible so it barely slows the user
down. But inputting dates like "mar 10" instead of
"3 10" keeps it from ever opening. And other tricks
like putting a comma after the day, like "3 10, 1876".
Even "10, 3 1876" needs no clarification.
'''
self.clarifier_is_running = True
self.clarifier = tk.Toplevel(padx=24, pady=24)
self.clarifier.grab_set()
self.clarifier.title('Clarify Date Parts')
self.clarifier.geometry('+800+300') # center this in screen
self.make_iso_frame()
self.ok = wdg.Button(
self.clarifier,
text='OK',
width=6,
command=self.send_date)
self.ok.grid(column=0, row=2, sticky='w', padx=12, pady=(12,0), ipadx=3)
x = wdg.Button(
self.clarifier,
text='CANCEL',
command=self.close_iso,
width=6)
x.grid(column=2, row=2, sticky='e', padx=12, pady=(12,0), ipadx=3)
self.clarifier.bind('<Return>', self.send_date)
ST.config_generic(self.clarifier)
# DATE PREFERENCES TAB IN PREFERENCES TAB
def revert_to_default(this_tree_or_all):
# current_file = files.get_current_file()[0]
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute('DELETE FROM date_per_tree')
conn.commit()
cur.execute('''
INSERT INTO date_per_tree
VALUES (1, null, null, null, null, null, null, null, null, null)''')
conn.commit()
cur.close()
conn.close()
# conn = sqlite3.connect(st.conn_fig)
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('DELETE FROM date_per_user')
conn.commit()
cur.execute('''
INSERT INTO date_per_user
VALUES (1, null, null, null, null, null, null, null, null, null)''')
conn.commit()
cur.close()
conn.close()
this_tree_or_all.set('local')
for combo in date_pref_combos.values():
if len(combo.get()) != 0:
combo.delete_content()
def get_rad_val(this_tree_or_all):
val = this_tree_or_all.get()
return val
def submit_date_prefs(this_tree_or_all):
for combo in date_pref_combos.values():
if len(combo.get()) != 0:
var_form = combo.get()
if combo == date_pref_combos['General']:
date_form = var_form
for k,v in DATE_FORMAT_LOOKUP.items():
if date_form == k:
date_form = v
elif combo == date_pref_combos['Estimated']:
est_form = var_form
elif combo == date_pref_combos['Approximate']:
abt_form = var_form
elif combo == date_pref_combos['Calculated']:
cal_form = var_form
elif combo == date_pref_combos['Before/After']:
befaft_form = var_form
elif combo == date_pref_combos['Epoch']:
epoch_form = var_form
elif combo == date_pref_combos['Julian/Gregorian']:
julegreg_form = var_form
elif combo == date_pref_combos['From...To...']:
span_form = var_form
for k,v in SPAN_FORMAT_LOOKUP.items():
if span_form == k:
span_form = v
elif combo == date_pref_combos['Between...And...']:
range_form = var_form
for k,v in RANGE_FORMAT_LOOKUP.items():
if range_form == k:
range_form = v
val = get_rad_val(this_tree_or_all)
if val == 'local' and len(combo.get()) != 0:
current_file = files.get_current_file()[0]
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
if combo is date_pref_combos['General']:
cur.execute('''
UPDATE date_per_tree
SET date_format = ?
WHERE date_per_tree_id = 1''',
(date_form,))
elif combo is date_pref_combos['Estimated']:
cur.execute('''
UPDATE date_per_tree
SET est = ?
WHERE date_per_tree_id = 1''',
(est_form,))
elif combo is date_pref_combos['Approximate']:
cur.execute('''
UPDATE date_per_tree
SET abt = ?
WHERE date_per_tree_id = 1''',
(abt_form,))
elif combo is date_pref_combos['Calculated']:
cur.execute('''
UPDATE date_per_tree
SET cal = ?
WHERE date_per_tree_id = 1''',
(cal_form,))
elif combo is date_pref_combos['Before/After']:
cur.execute('''
UPDATE date_per_tree
SET bef_aft = ?
WHERE date_per_tree_id = 1''',
(befaft_form,))
elif combo is date_pref_combos['Epoch']:
cur.execute('''
UPDATE date_per_tree
SET bc_ad = ?
WHERE date_per_tree_id = 1''',
(epoch_form,))
elif combo is date_pref_combos['Julian/Gregorian']:
cur.execute('''
UPDATE date_per_tree
SET os_ns = ?
WHERE date_per_tree_id = 1''',
(julegreg_form,))
elif combo is date_pref_combos['From...To...']:
cur.execute('''
UPDATE date_per_tree
SET span = ?
WHERE date_per_tree_id = 1''',
(span_form,))
elif combo is date_pref_combos['Between...And...']:
cur.execute('''
UPDATE date_per_tree
SET range = ?
WHERE date_per_tree_id = 1''',
(range_form,))
conn.commit()
cur.close()
conn.close()
elif val == 'global' and len(combo.get()) != 0:
# conn = sqlite3.connect(st.conn_fig)
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
if combo is date_pref_combos['General']:
cur.execute('''
UPDATE date_per_user
SET date_format = ?
WHERE date_per_user_id = 1 ''',
(date_form,))
elif combo is date_pref_combos['Estimated']:
cur.execute('''
UPDATE date_per_user
SET est = ?
WHERE date_per_user_id = 1 ''',
(est_form,))
elif combo is date_pref_combos['Approximate']:
cur.execute('''
UPDATE date_per_user
SET abt = ?
WHERE date_per_user_id = 1 ''',
(abt_form,))
elif combo is date_pref_combos['Calculated']:
cur.execute('''
UPDATE date_per_user
SET cal = ?
WHERE date_per_user_id = 1 ''',
(cal_form,))
elif combo is date_pref_combos['Before/After']:
cur.execute('''
UPDATE date_per_user
SET bef_aft = ?
WHERE date_per_user_id = 1 ''',
(befaft_form,))
elif combo is date_pref_combos['Epoch']:
cur.execute('''
UPDATE date_per_user
SET bc_ad = ?
WHERE date_per_user_id = 1 ''',
(epoch_form,))
elif combo is date_pref_combos['Julian/Gregorian']:
cur.execute('''
UPDATE date_per_user
SET os_ns = ?
WHERE date_per_user_id = 1 ''',
(julegreg_form,))
elif combo is date_pref_combos['From...To...']:
cur.execute('''
UPDATE date_per_user
SET span = ?
WHERE date_per_user_id = 1 ''',
(span_form,))
elif combo is date_pref_combos['Between...And...']:
cur.execute('''
UPDATE date_per_user
SET range = ?
WHERE date_per_user_id = 1 ''',
(range_form,))
conn.commit()
cur.close()
conn.close()
# why is connection being closed and reopened here?
current_file = files.get_current_file()[0]
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
if combo is date_pref_combos['General']:
cur.execute('''
UPDATE date_per_tree
SET date_format = NULL
WHERE date_per_tree_id = 1''')
elif combo is date_pref_combos['Estimated']:
cur.execute('''
UPDATE date_per_tree
SET est = NULL
WHERE date_per_tree_id = 1''')
elif combo is date_pref_combos['Approximate']:
cur.execute('''
UPDATE date_per_tree
SET abt = NULL
WHERE date_per_tree_id = 1''')
elif combo is date_pref_combos['Calculated']:
cur.execute('''
UPDATE date_per_tree
SET cal = NULL
WHERE date_per_tree_id = 1''')
elif combo is date_pref_combos['Before/After']:
cur.execute('''
UPDATE date_per_tree
SET bef_aft = NULL
WHERE date_per_tree_id = 1''')
elif combo is date_pref_combos['Epoch']:
cur.execute('''
UPDATE date_per_tree
SET bc_ad = NULL
WHERE date_per_tree_id = 1''')
elif combo is date_pref_combos['Julian/Gregorian']:
cur.execute('''
UPDATE date_per_tree
SET os_ns = NULL
WHERE date_per_tree_id = 1''')
elif combo is date_pref_combos['From...To...']:
cur.execute('''
UPDATE date_per_tree
SET span = NULL
WHERE date_per_tree_id = 1''')
elif combo is date_pref_combos['Between...And...']:
cur.execute('''
UPDATE date_per_tree
SET range = NULL
WHERE date_per_tree_id = 1''')
conn.commit()
cur.close()
conn.close()
combo.delete_content()
this_tree_or_all.set('local')
def format_misc_date(iso_date):
date_show = ValidDate()
if '-and-' not in iso_date and '-to-' not in iso_date:
formatted_date = date_show.format_date_for_table(iso_date)
elif '-and-' in iso_date:
date_show.and_to = ' and '
elif '-to-' in iso_date:
date_show.and_to = ' to '
if date_show.and_to:
formatted_date = date_show.select_function(iso_date)
return formatted_date
formats = st.make_formats_dict()