PythonBeginner · Windows, macOS, Linux; Python 3.12+

Compare daily work windows

Find the UTC overlap between two local work windows on an explicit date.

Before you begin

  • Python 3.12+
  • IANA time-zone data; if missing, install with python -m pip install tzdata.
What this script changesReads the chosen input and creates a new result. Existing outputs are refused. Reports can contain source names or text, so store them appropriately.

Set up your example

  1. Save the complete code as compare-local-working-windows.py.
  2. Open a terminal in the folder containing the script.

This recipe uses your own local input or creates its own example output. Follow the setup steps below.

Download the ZIP, extract it, and open a terminal inside the extracted folder. The source file is compare-local-working-windows.py.

Run the script

Run command
python compare-local-working-windows.py --date 2026-10-24 --zone-a America/Chicago --zone-b Europe/London --output "overlap.txt"

If your system uses python3 instead of python, use that command for Python examples. Keep paths containing spaces in quotes. Required inputs and optional settings are described in the code and setup steps.

What to expect

For 09:00–17:00 in both cities on October 24, overlap is 14:00–16:00 UTC, or 120 minutes.

The complete script

compare-local-working-windows.py
import argparse, ast, collections, difflib, fnmatch, gzip, hashlib, io, json, math, os, random, re, shutil, statistics, string, sys, tarfile, textwrap, time, unicodedata, zipfile
from pathlib import Path
import xml.etree.ElementTree as ET

def write_text(path, content):
    path=Path(path)
    path.parent.mkdir(parents=True,exist_ok=True)
    with path.open('x',encoding='utf-8',newline='') as h: h.write(content)
    print('Created',path)

def fresh_dir(path):
    path=Path(path); path.mkdir(parents=True,exist_ok=False); return path

def files_under(root):
    root=Path(root)
    if not root.is_dir(): raise ValueError('Input must be a directory')
    return sorted((p for p in root.rglob('*') if p.is_file() and not p.is_symlink()),key=lambda p:p.as_posix().casefold())

def digest(path):
    h=hashlib.sha256()
    with Path(path).open('rb') as f:
        for b in iter(lambda:f.read(1048576),b''): h.update(b)
    return h.hexdigest()

def positive(n,name):
    if n<=0: raise ValueError(name+' must be positive')

def destination(root,name):
    name=name.replace('\\','/')
    if name.startswith('/') or any(p=='..' or ':' in p for p in name.split('/')): raise ValueError('Unsafe path: '+name)
    p=(root/name).resolve()
    if p!=root.resolve() and root.resolve() not in p.parents: raise ValueError('Path escapes destination')
    return p

def zip_members(z,limit=500000000):
    seen=set(); total=0; result=[]
    for i in z.infolist():
        if i.is_dir(): continue
        destination(Path.cwd(),i.filename)
        key=i.filename.replace('\\','/').casefold()
        if key in seen: raise ValueError('Duplicate/case-colliding member: '+i.filename)
        seen.add(key)
        if ((i.external_attr>>16)&0o170000)==0o120000: raise ValueError('Symlinks unsupported')
        total+=i.file_size
        if total>limit: raise ValueError('Expanded data exceeds limit')
        result.append(i)
    return result

def copy_member(z,i,root):
    p=destination(root,i.filename); p.parent.mkdir(parents=True,exist_ok=True)
    with z.open(i) as src,p.open('xb') as dst: shutil.copyfileobj(src,dst)

def read_input(a): return a.input.read_text(encoding='utf-8-sig')
NS={'w':'http://schemas.openxmlformats.org/wordprocessingml/2006/main','r':'http://schemas.openxmlformats.org/officeDocument/2006/relationships'}
def docx_root(path,member='word/document.xml'):
    with zipfile.ZipFile(path) as z:
        i=z.getinfo(member)
        if i.file_size>30000000: raise ValueError('Document XML exceeds 30 MB')
        return ET.fromstring(z.read(i))
def pdf_reader(path):
    try: from pypdf import PdfReader
    except ImportError as e: raise ValueError('Install dependency: python -m pip install pypdf') from e
    r=PdfReader(path)
    if r.is_encrypted: raise ValueError('Encrypted PDF unsupported')
    return r
def pdf_writer():
    try: from pypdf import PdfWriter
    except ImportError as e: raise ValueError('Install dependency: python -m pip install pypdf') from e
    return PdfWriter()
def save_pdf(w,p):
    Path(p).parent.mkdir(parents=True,exist_ok=True)
    with Path(p).open('xb') as h: w.write(h)
    print('Created',p)

def md_headings(content):
    fence=None
    for number,line in enumerate(content.splitlines(),1):
        mark=re.match(r'^\s{0,3}(`{3,}|~{3,})',line)
        if mark:
            char=mark.group(1)[0]
            if fence is None: fence=char
            elif fence==char: fence=None
            continue
        if fence is None:
            m=re.match(r'^(#{1,6})\s+(.+?)\s*#*\s*$',line)
            if m: yield number,len(m.group(1)),m.group(2)

import calendar, csv
from datetime import date,datetime,timedelta,timezone
from zoneinfo import ZoneInfo

def csv_rows(path,required=()):
    with Path(path).open(encoding='utf-8-sig',newline='') as f:
        reader=csv.DictReader(f)
        if not reader.fieldnames or any(k not in reader.fieldnames for k in required): raise ValueError('Required columns: '+', '.join(required))
        return list(reader)

def csv_text(rows,columns):
    out=io.StringIO(newline='');writer=csv.DictWriter(out,fieldnames=columns);writer.writeheader();writer.writerows(rows);return out.getvalue()

def aware(text):
    value=datetime.fromisoformat(text.replace('Z','+00:00'))
    if value.tzinfo is None: raise ValueError('Timestamp must include UTC offset or Z: '+text)
    return value.astimezone(timezone.utc)

def date_range(start,end):
    if end<start: raise ValueError('End precedes start')
    if (end-start).days>366000: raise ValueError('Date range exceeds 1000 years')
    for i in range((end-start).days+1): yield start+timedelta(days=i)

def ical_events(path):
    lines=re.sub(r'\r?\n[ \t]','',Path(path).read_text(encoding='utf-8-sig')).splitlines();events=[];current=None
    for line in lines:
        if line=='BEGIN:VEVENT':
            if current is not None: raise ValueError('Nested VEVENT')
            current=[]
        elif line=='END:VEVENT':
            if current is None: raise ValueError('END without VEVENT')
            events.append(current);current=None
        elif current is not None: current.append(line)
    if current is not None: raise ValueError('Unclosed VEVENT')
    return events

def ical_value(lines,key):
    return next((line.split(':',1)[1] for line in lines if line.startswith(key+':') or line.startswith(key+';')), '')

def ical_escape(text): return text.replace('\\','\\\\').replace('\n','\\n').replace(';','\\;').replace(',','\\,')
def ical_fold(line):
    parts=[];current=''
    for char in line:
        if len((current+char).encode('utf-8'))>75: parts.append(current);current=' '+char
        else: current+=char
    parts.append(current);return '\r\n'.join(parts)

def main():
    parser=argparse.ArgumentParser(description='Find the UTC overlap between two local work windows on an explicit date.')
    parser.add_argument('--output',type=Path,required=True,help='New result path; must not exist')
    parser.add_argument('--date',required=True)
    parser.add_argument('--zone-a',required=True)
    parser.add_argument('--zone-b',required=True)
    parser.add_argument('--start-a',default='09:00')
    parser.add_argument('--end-a',default='17:00')
    parser.add_argument('--start-b',default='09:00')
    parser.add_argument('--end-b',default='17:00')
    args=parser.parse_args()
    day=date.fromisoformat(args.date)
    def window(zone,start,end):
        z=ZoneInfo(zone);a=datetime.combine(day,datetime.strptime(start,'%H:%M').time(),z);b=datetime.combine(day,datetime.strptime(end,'%H:%M').time(),z)
        if b<=a: raise ValueError('Windows must end after start on the same local date')
        return a.astimezone(timezone.utc),b.astimezone(timezone.utc)
    a,b=window(args.zone_a,args.start_a,args.end_a);c,d=window(args.zone_b,args.start_b,args.end_b);start=max(a,c);end=min(b,d)
    write_text(args.output,('No overlap\n' if end<=start else f'Overlap UTC: {start.isoformat()} to {end.isoformat()}\nMinutes: {(end-start).total_seconds()/60:g}\n'))

if __name__=='__main__':
    try: main()
    except (OSError,ValueError,KeyError,zipfile.BadZipFile,tarfile.TarError,ET.ParseError) as error:
        print('Error:',error,file=sys.stderr); sys.exit(2)

Open source as plain text · Download source and sample files

How it works

  1. Uses both zones on the same explicit local date.
  2. Change --zone-a, --zone-b and their --start/--end flags for each work window.
  3. Choose ordinary daytime hours; this helper does not resolve ambiguous or nonexistent clock times at daylight-saving transitions.

Make it your own

Change the task behavior

Change --date and the four start/end flags to compare different local work windows.

Choose work windows

Change --start-a, --end-a, --start-b, and --end-b using HH:MM.

Choose zones and date

Change --zone-a, --zone-b, and --date. Windows need available zoneinfo data; install tzdata if needed.

Change one setting at a time, run it again on sample data, and compare the output. The customization guide walks through this process.

If something goes wrong

The chosen source cannot be read.

Check the source path, quote paths with spaces, and confirm the file or folder is readable.

The output already exists.

Use a new --output path; existing results are deliberately preserved.

A task-specific argument is rejected.

Run python compare-local-working-windows.py --help, then compare the options with the complete example command and supplied sample format.

Reference & next steps

These official references explain the underlying tools. The Scriptage example and guide are original instructional content. See verification notes for what has been checked and the limits of those checks.

Copies a template for your own notes. It does not send anything.