1.1.1 버전에서는 데이터의 저장을 사용자의 수동 처리에 의존하고 있었기 때문에, 사용자가 깜빡하거나 일부러 데이터를 입력하지 않았을 경우, 시계열 데이터에 흠결이 생긴다. 또한 데이터셋의 날짜 컬럼을 다루는 로직을 설계하는 일이 까다로워진다. 따라서 데이터 처리 과정을 매끄럽게 하고, 신뢰성 있는 결과를 도출하기 위해, 데이터가 저장되지 않은 날짜의 데이터를 자동으로 저장하는 기능을 넣었다.
1일 1백준에 아주 커다란 동기부여가 되어 준 solved.ac의 [스트릭]을 프로그램에 넣어 봤다. 내가 어느 시기에 소홀했고, 어느 시기에 꾸준했는지를 한 눈에 볼 수 있다는 장점이 있다. 현재 연속 기록을 경신하고 있다면, 계속 이어가기 위해서라도 꼭 하게 되는 효과도 있다.
요약
미입력 데이터 자동 업데이트 및 플래그 스트릭 출력 기능 추가
사용 언어
Python
라이브러리
- csv
- datetime
- pytz
- numpy
- pandas
- matplotlib
- seaborn
새로운 기능
- 미입력 데이터 자동 업데이트
- 사용자가 할 일 목록 및 해낸 일 목록만 채우고 데이터 저장을 하지 않았을 경우
- 해당 날짜에 데이터를 저장하고 초기화함
- 사용자가 아무 것도 하지 않았을 경우
- 해당 날짜에 빈 배열과 숫자 0을 저장함
- 데이터 업데이트 시점
- 프로그램이 실시간으로 돌아갈 수 없으므로, 사용자가 프로그램을 실행시켰을 때, 지난 날짜들에 대해 일괄적으로 저장되지 않은 데이터를 추가함
- 사용자가 할 일 목록 및 해낸 일 목록만 채우고 데이터 저장을 하지 않았을 경우
- 플래그 스트릭
- 지난 1년간 하루 할 일 관리를 한 날에 깃발을 꽂음
- 꽂힌 깃발들을 보여줌
- 할 일 관리의 최대 연속 일수, 현재 연속 일수를 표시함
내부 변경 사항
- 데이터 시각화 디자인을 위해 pandas와 seaborn 라이브러리 사용
- 요일별 분류가 용이하도록, data.csv의 날짜 컬럼에서 요일 정보를 제외하고 요일 컬럼을 추가함
- 파일 경로 추적이 용이하도록, 파일 경로를 나타내는 변수 선언
- pytz 라이브러리를 사용하여, date 라이브러리로 호출되는 모든 시간이 한국 표준시에 따름
# © 2022 starsein <dbtjd1928@gmail.com>
import csv
import datetime as dt
import pytz
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from typing import List, Tuple
# 입출력에 사용되는 파일 목록
DATA_CSV = 'data.csv'
TODO_LIST_CSV = 'todoList.csv'
COMPLETED_TASK_LIST_CSV = 'completedTaskList.csv'
# data.csv 의 컬럼별 항목 분류
DATE = 0
WEEKDAY = 1
UNCOMPLETED_TASK_LIST = 2
UNCOMPLETED_TASK_NUM = 3
COMPLETED_TASK_LIST = 4
COMPLETED_TASK_NUM = 5
def update_data():
with open(DATA_CSV, 'r', encoding='utf-8-sig') as rf:
total_data = rf.readlines()
if len(total_data) == 0:
return
recent_date_str = total_data[-1].split(',')[DATE]
today_obj = dt.date.fromisoformat(get_today_weekday()[0])
target_day_obj = dt.date.fromisoformat(recent_date_str) + dt.timedelta(days=1)
updating_data_list = []
while target_day_obj < today_obj:
target_day_str = target_day_obj.strftime("%Y-%m-%d")
target_weekday_str = weekday_en_to_kr(target_day_obj.isoweekday())
updating_data_list.append([target_day_str, target_weekday_str, [], 0, [], 0])
target_day_obj += dt.timedelta(days=1)
if len(updating_data_list):
unsaved_todo = get_todo()
unsaved_completed_task = get_completed_task()
updating_data_list[0][2] = unsaved_todo
updating_data_list[0][3] = len(unsaved_todo)
updating_data_list[0][4] = unsaved_completed_task
updating_data_list[0][5] = len(unsaved_completed_task)
with open(TODO_LIST_CSV, 'w', encoding='utf-8-sig', newline=''):
pass
with open(COMPLETED_TASK_LIST_CSV, 'w', encoding='utf-8-sig', newline=''):
pass
with open(DATA_CSV, 'a', encoding='utf-8-sig', newline='') as af:
writer = csv.writer(af)
writer.writerows(updating_data_list)
def add_todo():
print("[할 일 추가]")
with open(TODO_LIST_CSV, 'a', encoding='utf-8-sig', newline='') as af:
writer = csv.writer(af)
print("오늘 할 일을 입력하세요.",
"이전 메뉴로 돌아가려면 q 또는 Q를 입력하세요.", sep='\n')
while True:
todo_str = input().rstrip()
if todo_str == 'q' or todo_str == 'Q':
return 0
writer.writerow([todo_str])
print(f"{todo_str}가 할 일 목록에 정상적으로 추가되었습니다!")
def get_todo() -> List[str]:
with open(TODO_LIST_CSV, 'r', encoding='utf-8-sig') as rf:
todo_list = []
reader = csv.reader(rf)
for todo in reader:
todo_list.append(*todo)
return todo_list
def show_todo():
print("[할 일 확인]")
todo_list = get_todo()
print("+------------------------------------+")
for idx, todo in enumerate(todo_list, start=1):
print(idx, todo)
print("+------------------------------------+")
while True:
cmd = input("이전 메뉴로 돌아가려면 q 또는 Q를 입력하세요.\n").rstrip()
if cmd == 'q' or cmd == 'Q':
return 0
def add_completed_task():
print("[해낸 일 추가]")
todo_list = get_todo()
with open(COMPLETED_TASK_LIST_CSV, 'a', encoding='utf-8-sig', newline='') as af:
writer = csv.writer(af)
print("오늘 해낸 일을 입력하세요.",
"이전 메뉴로 돌아가려면 q 또는 Q를 입력하세요.", sep='\n')
while True:
print("[현재 할 일 목록]")
print("+------------------------------------+")
for idx, todo in enumerate(todo_list, start=1):
print(idx, todo)
print("+------------------------------------+")
todo_str = input().rstrip()
if todo_str == 'q' or todo_str == 'Q':
break
if todo_str not in todo_list:
print("오늘 할 일에 없는 입력입니다.")
continue
todo_list.remove(todo_str)
writer.writerow([todo_str])
with open(TODO_LIST_CSV, 'w', encoding='utf-8-sig', newline='') as wf:
writer = csv.writer(wf)
for todo in todo_list:
writer.writerow([todo])
def get_completed_task() -> List[str]:
with open(COMPLETED_TASK_LIST_CSV, 'r', encoding='utf-8-sig') as rf:
ct_list = []
reader = csv.reader(rf)
for completed_task in reader:
ct_list.append(*completed_task)
return ct_list
def show_completed_task():
print("[해낸 일 확인]")
ct_list = get_completed_task()
print("+------------------------------------+")
for idx, completed_task in enumerate(ct_list, start=1):
print(idx, completed_task)
print("+------------------------------------+")
while True:
cmd = input("이전 메뉴로 돌아가려면 q 또는 Q를 입력하세요.\n").rstrip()
if cmd == 'q' or cmd == 'Q':
return 0
def check_data(today_str: str) -> Tuple[str, List[List[str]]]:
with open(DATA_CSV, "r", encoding='utf-8-sig') as rf:
data_list = []
reader = csv.reader(rf)
for day_data in reader:
data_list.append(day_data)
date = day_data[0]
if date == today_str:
print("현재 날짜에 이미 저장된 데이터가 있습니다.")
while True:
user_cmd = input("새로운 데이터로 덮어쓰기 하시겠습니까? [y/n]").rstrip()
if user_cmd == 'y':
return "OVERWRITE", data_list
elif user_cmd == 'n':
return "DON\'T OVERWRITE", data_list
return "NOT TO OVERWRITE", data_list
def weekday_en_to_kr(weekday_int: int) -> str:
translate_table = {1: "월",
2: "화",
3: "수",
4: "목",
5: "금",
6: "토",
7: "일"}
return translate_table.get(weekday_int)
def get_today_weekday() -> Tuple[str, str]:
KST = pytz.timezone('Asia/Seoul')
info = dt.datetime.now(KST)
today_str = info.strftime("%Y-%m-%d")
weekday_str = weekday_en_to_kr(info.isoweekday())
return today_str, weekday_str
def store_data():
today_str, weekday_str = get_today_weekday()
todo_list = get_todo()
ct_list = get_completed_task()
res, data_list = check_data(today_str)
if res == "OVERWRITE":
data_list.pop()
elif res == "DON\'T OVERWRITE":
return 0
else:
pass
data_list.append([today_str, weekday_str, todo_list, len(todo_list), ct_list, len(ct_list)])
with open(DATA_CSV, 'w', encoding='utf-8-sig', newline='') as wf:
writer = csv.writer(wf)
writer.writerows(data_list)
with open(TODO_LIST_CSV, 'w', encoding='utf-8-sig', newline=''):
pass
with open(COMPLETED_TASK_LIST_CSV, 'w', encoding='utf-8-sig', newline=''):
pass
print("오늘의 데이터 집계 및 초기화가 완료되었습니다!")
def visualize_data():
date_arr = np.array([])
num_t_arr = np.array([])
num_ct_arr = np.array([])
cr_arr = np.array([])
with open(DATA_CSV, 'r') as rf:
reader = csv.reader(rf)
for data in reader:
stored_date, stored_weekday, ut, num_ut, ct, num_ct = data
num_ut = int(num_ut)
num_ct = int(num_ct)
date_arr = np.append(date_arr, stored_date)
num_t_arr = np.append(num_t_arr, num_ut + num_ct)
num_ct_arr = np.append(num_ct_arr, num_ct)
cr = round(num_ct / (num_ct + num_ut) * 100) if num_ct | num_ut != 0 else 0
cr_arr = np.append(cr_arr, cr)
print("[현재까지 집계된 데이터 시각화]")
print(f"총 {len(date_arr)}개 날짜의 데이터가 저장되어 있습니다.")
user_cmd = int(input("최근에 저장된 데이터를 몇 개까지 표시할까요?\n"))
date_arr = date_arr[-user_cmd:]
num_ct_arr = num_ct_arr[-user_cmd:]
num_t_arr = num_t_arr[-user_cmd:]
cr_arr = cr_arr[-user_cmd:]
data_dict = {'date': date_arr,
'num_t': num_t_arr,
'num_ct': num_ct_arr,
'cr': cr_arr}
data_df = pd.DataFrame(data=data_dict)
plt.figure(figsize=(16, 8))
plt.title("TodoTracker", fontsize=25)
sns.set_style('darkgrid')
sns.set_context('notebook')
sns.barplot(x='date', y='num_t', data=data_df, palette='OrRd')
sns.barplot(x='date', y='num_ct', data=data_df, palette='GnBu')
sns.lineplot(x='date', y='cr', data=data_df, color='limegreen', marker='o', linestyle='-.')
for i, v in enumerate(date_arr):
plt.text(v, cr_arr[i], f"{int(cr_arr[i])}%", fontsize=13, fontfamily='monospace', horizontalalignment='center',
verticalalignment='bottom', color='limegreen')
plt.xlabel("Date", fontsize=15)
plt.ylabel(" ")
plt.xticks(rotation=45)
plt.show()
def show_date_streak():
presence_date_set = set()
with open(DATA_CSV, 'r') as rf:
reader = csv.reader(rf)
curr_cnt = 0
max_cnt = 0
for day_data in reader:
_date = day_data[0]
presence_date_set.add(_date)
if day_data[COMPLETED_TASK_NUM]:
curr_cnt += 1
max_cnt = max(max_cnt, curr_cnt)
else:
curr_cnt = 0
NUM_WEEK = 53
NUM_WEEKDAY = 7
matrix = [[" " for row in range(NUM_WEEK + 2)] for col in range(NUM_WEEKDAY + 1)]
matrix[0][0], matrix[1][0], matrix[2][0], matrix[3][0], matrix[4][0], matrix[5][0], matrix[6][0]\
= "S ", "M ", "T ", "W ", "T ", "F ", "S "
today_obj = dt.date.fromisoformat(get_today_weekday()[0])
a_year_ago_obj = today_obj - dt.timedelta(days=364)
a_year_ago_weekday = a_year_ago_obj.isoweekday() % 7
col = a_year_ago_weekday
row = 2
curr_day_obj = a_year_ago_obj
while curr_day_obj <= today_obj:
curr_day_str = curr_day_obj.strftime("%Y-%m-%d")
BLACK_FLAG = "\u2691 "
WHITE_FLAG = "\u2690 "
if curr_day_str in presence_date_set:
matrix[col][row] = BLACK_FLAG
else:
matrix[col][row] = WHITE_FLAG
if curr_day_obj.day == 1:
if curr_day_obj.month == 1:
matrix[7][row] = f"{curr_day_obj.year}"
try:
matrix[7][row+1] = ""
except IndexError:
pass
else:
matrix[7][row] = f"{curr_day_obj.month}월"
try:
matrix[7][row+1] = " "
except IndexError:
pass
curr_day_obj += dt.timedelta(days=1)
col += 1
if col == 7:
col = 0
row += 1
print(f"최대 {max_cnt}일 연속 하루 관리, 현재 {curr_cnt}일", end=' ')
if max_cnt == curr_cnt:
print("!!!\n")
else:
print()
for c in range(NUM_WEEKDAY + 1):
for r in range(NUM_WEEK + 2):
print(matrix[c][r], end='')
print()
def main():
update_data()
func_str_dict = {1: "할 일 추가",
2: "할 일 확인",
3: "해낸 일 추가",
4: "해낸 일 확인",
5: "오늘의 데이터 집계 및 초기화",
6: "현재까지 집계된 데이터 시각화",
7: "플래그 스트릭 조회"}
func_exec_dict = {1: "add_todo()",
2: "show_todo()",
3: "add_completed_task()",
4: "show_completed_task()",
5: "store_data()",
6: "visualize_data()",
7: "show_date_streak()"}
while True:
current_date, cw = get_today_weekday()
cy, cm, cd = current_date.split('-')
print("+--------------------+",
"| TodoTracker v1.1.2 |",
"+--------------------+",
f"# 오늘은 {cy}년 {cm}월 {cd}일 {cw}요일",
"사용하고자 하는 기능의 번호를 입력하세요!",
"프로그램을 종료하려면 기능의 번호 이외의 숫자나 문자를 입력하세요.", sep='\n')
print("+--+---------------------------+")
for func_key, func_str in func_str_dict.items():
print(f"|{func_key:>2d}|{func_str}")
print("+--+---------------------------+")
try:
user_cmd = int(input())
except ValueError:
break
try:
exec(func_exec_dict[user_cmd])
except KeyError:
break
print("프로그램을 종료합니다.",
"이용해주셔서 감사합니다!", sep='\n')
return 0
if __name__ == '__main__':
main()
스트릭을 구현할 때는 뭔가 알고리즘 구현 문제를 푸는 것 같은 재미와 골치아픔이 있었다. 즐거운 코딩이었다. 과제가 아니니까
'내가 가는 여정을 담는 그릇' 카테고리의 다른 글
[나의 작은 프로그래밍] TodoTracker (v.1.2.1) (0) | 2022.03.26 |
---|---|
2022 SK ICT Family 개발자 채용 챌린지 - 2차 코딩 테스트 후기 (0) | 2022.03.19 |
이번 학기 첫 코드 리뷰 (0) | 2022.03.15 |
2022 SK ICT Family 개발자 채용 챌린지 - 1차 코딩 테스트 후기 (0) | 2022.03.12 |
[나의 작은 프로그래밍] TodoTracker (v.1.1.1) (0) | 2022.03.10 |