EventBuzz_Backend/scripts/parse-dehradun-excel-agenda.py

254 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Parse Designing detailed Dehradun Agenda.xlsx into nested session/topic JSON."""
import json
import re
import zipfile
import xml.etree.ElementTree as ET
from pathlib import Path
XLSX = Path(
__import__('os').environ.get(
'DEHRADUN_AGENDA_XLSX',
'/Volumes/Storage/Downloads/Designing detailed Dehradun Agenda.xlsx',
)
)
NS = {'m': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
TOPIC_TYPES = {
'LECTURE',
'PANEL DISCUSSION',
'JAMA',
'INTRODUCTION',
'TALK',
'TALK + Q&A',
'CASE PRESENTATION+DISCUSSION',
'CASE PRESENTATION + DISCUSSION',
}
DAY_DATES = ['September 19, 2026', 'September 20, 2026']
def load_rows(path):
with zipfile.ZipFile(path) as z:
ss_root = ET.fromstring(z.read('xl/sharedStrings.xml'))
strings = []
for si in ss_root.findall('m:si', NS):
strings.append(''.join(t.text or '' for t in si.findall('.//m:t', NS)))
sheet = ET.fromstring(z.read('xl/worksheets/sheet1.xml'))
rows = []
for row in sheet.findall('m:sheetData/m:row', NS):
cells = {}
for c in row.findall('m:c', NS):
ref = c.get('r')
col = re.match(r'[A-Z]+', ref).group(0)
t = c.get('t')
v = c.find('m:v', NS)
is_el = c.find('m:is', NS)
val = ''
if t == 's' and v is not None:
val = strings[int(v.text)]
elif t == 'inlineStr' and is_el is not None:
val = ''.join(t2.text or '' for t2 in is_el.findall('.//m:t', NS))
elif v is not None:
val = v.text or ''
cells[col] = val
rows.append((int(row.get('r')), cells))
return rows
def clean(val):
return re.sub(r'[ \t]+', ' ', str(val or '')).replace('\xa0', ' ').strip()
def normalize_clock(part, day_index):
text = clean(part).replace(' :', ':')
m = re.search(r'(\d{1,2}):\s*(\d{2})\s*(AM|PM)?', text, re.I)
if not m:
return text
hour = int(m.group(1))
minute = int(m.group(2))
suffix = (m.group(3) or '').upper()
if day_index == 0 and 1 <= hour <= 6:
suffix = 'PM'
elif day_index >= 1:
if 9 <= hour <= 11:
suffix = 'AM'
elif hour == 12:
suffix = 'PM'
elif not suffix:
suffix = 'PM' if 1 <= hour <= 6 else 'AM'
hour12 = hour if hour <= 12 else hour - 12
return f'{hour12}:{minute:02d} {suffix}'
def normalize_range(raw, day_index):
text = clean(raw).replace(' :', ':')
if not text:
return ''
parts = re.split(r'\s*[–—-]\s*', text)
if len(parts) < 2:
return normalize_clock(text, day_index)
return f'{normalize_clock(parts[0], day_index)} – {normalize_clock(parts[1], day_index)}'
def minutes_between(range_str):
parts = re.split(r'\s*[–—]\s*', range_str)
if len(parts) < 2:
return ''
clocks = []
for part in parts[:2]:
m = re.search(r'(\d{1,2}):(\d{2})\s*(AM|PM)', part, re.I)
if not m:
return ''
hour = int(m.group(1)) % 12
if m.group(3).upper() == 'PM':
hour += 12
clocks.append(hour * 60 + int(m.group(2)))
delta = clocks[1] - clocks[0]
return str(delta) if delta > 0 else ''
def is_topic_type(theme):
return clean(theme).upper() in TOPIC_TYPES or clean(theme).upper().startswith('TALK')
def is_session_header(time, theme, title, faculty):
blob = '\n'.join([time, theme])
if re.search(r'^SESSION\s+', blob, re.I | re.M):
return True
if re.search(r'INTRODUCTION TO THE NOVEL LUNG HEALTH SUMMIT', blob, re.I):
return True
return False
def session_name_from(text):
lines = [clean(line) for line in str(text or '').splitlines() if clean(line)]
if not lines:
return ''
name = lines[0]
if len(lines) > 1:
extra = ' '.join(lines[1:])
name = f'{name} — {extra}'
return name
def faculty_role(topic_type, faculty):
role = clean(faculty).strip(' ,')
if not role:
return ''
kind = topic_type.upper()
if kind in {'PANEL DISCUSSION', 'JAMA'} and not re.match(r'panelists?:', role, re.I):
return f'Panelists: {role}'
return role
def parse_agenda(path):
rows = load_rows(path)
days = []
current_day = None
current_session = None
day_index = -1
def start_day(label, date):
nonlocal current_day, current_session, day_index
day_index = len(days)
current_day = {'label': label, 'date': date, 'sessions': []}
days.append(current_day)
current_session = None
def start_session(name):
nonlocal current_session
if not current_day:
start_day('Day 01', DAY_DATES[0])
current_session = {
'time': '',
'type': 'Main programme',
'title': '',
'name': name,
'topics': [],
'_times': [],
}
current_day['sessions'].append(current_session)
return current_session
def add_topic(topic_type, time_range, title, faculty):
if current_session is None:
return
if time_range:
current_session['_times'].append(time_range)
current_session['topics'].append({
'type': topic_type,
'duration': minutes_between(time_range) if time_range else '',
'title': title,
'role': faculty_role(topic_type, faculty),
})
for _, cells in rows:
time = clean(cells.get('B', ''))
theme = clean(cells.get('C', ''))
title = clean(cells.get('D', ''))
faculty = clean(cells.get('E', ''))
if not any([time, theme, title, faculty]):
continue
if time in {'Time'} or theme in {'Theme'}:
continue
day_match = re.match(r'DAY\s+(\d+)', time, re.I)
if day_match and not theme:
idx = int(day_match.group(1)) - 1
start_day(f'Day {idx + 1:02d}', DAY_DATES[idx] if idx < len(DAY_DATES) else '')
continue
if re.search(r'^TEA BREAK$', theme, re.I) or re.search(r'^TEA BREAK$', time, re.I):
sess = start_session('TEA BREAK')
sess['time'] = normalize_range(time, max(day_index, 0))
sess['topics'] = []
sess['_times'] = [sess['time']] if sess['time'] else []
continue
header_text = time if re.search(r'^SESSION\s+', time, re.I) else (
theme if re.search(r'INTRODUCTION TO THE NOVEL|SESSION\s+', theme, re.I) else ''
)
if header_text and not title and not faculty and not is_topic_type(theme):
start_session(session_name_from(header_text or theme or time))
continue
if is_session_header(time, theme, title, faculty) and not is_topic_type(theme):
start_session(session_name_from(theme or time))
if title or faculty:
add_topic('', normalize_range(time, max(day_index, 0)), title, faculty)
continue
if is_topic_type(theme) or title or faculty:
if current_session is None:
start_session(theme or title)
add_topic(
theme if is_topic_type(theme) else '',
normalize_range(time, max(day_index, 0)),
title,
faculty,
)
for day in days:
for sess in day['sessions']:
times = [t for t in sess.pop('_times', []) if t]
if times:
first = times[0].split(' – ')[0]
last = times[-1].split(' – ')[-1]
sess['time'] = f'{first} – {last}' if first != last or '–' in times[0] else times[0]
if sess['name'] == 'TEA BREAK':
sess['topics'] = []
return {'days': days}
def main():
data = parse_agenda(XLSX)
print(json.dumps(data, indent=2, ensure_ascii=False))
print('\nSUMMARY', file=__import__('sys').stderr)
for day in data['days']:
print(f"{day['label']} {day['date']} sessions={len(day['sessions'])}", file=__import__('sys').stderr)
for s in day['sessions']:
print(f" {s['time']} | {s['name'][:80]} | topics={len(s['topics'])}", file=__import__('sys').stderr)
if __name__ == '__main__':
main()