Add core application modules and structure
This commit is contained in:
@@ -1,5 +1,35 @@
|
||||
def main():
|
||||
print("Hello from custombingo!")
|
||||
import click
|
||||
import os
|
||||
from custom_bingo.card_generator import generate_bingo_cards
|
||||
from custom_bingo.spreadsheet_reader import read_spreadsheet
|
||||
from custom_bingo.pdf_generator import export_cards_to_pdf
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--input-file', '-i', required=True, type=click.Path(exists=True),
|
||||
help='Input spreadsheet file (Excel/CSV) with B, I, N, G, O columns')
|
||||
@click.option('--output-file', '-o', required=True, type=click.Path(),
|
||||
help='Output PDF file for the BINGO cards')
|
||||
@click.option('--number-of-cards', '-n', default=1, type=int,
|
||||
help='Number of BINGO cards to generate (default: 1)')
|
||||
def main(input_file, output_file, number_of_cards):
|
||||
"""Generate custom BINGO cards from a spreadsheet."""
|
||||
try:
|
||||
# Read the spreadsheet data
|
||||
click.echo(f"Reading data from {input_file}...")
|
||||
data = read_spreadsheet(input_file)
|
||||
|
||||
# Generate the bingo cards
|
||||
click.echo(f"Generating {number_of_cards} BINGO card(s)...")
|
||||
cards = generate_bingo_cards(data, number_of_cards)
|
||||
|
||||
# Export to PDF
|
||||
click.echo(f"Exporting cards to {output_file}...")
|
||||
export_cards_to_pdf(cards, output_file)
|
||||
|
||||
click.echo(f"Successfully generated {number_of_cards} BINGO card(s) in {output_file}")
|
||||
except Exception as e:
|
||||
click.echo(f"Error: {str(e)}", err=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -10,3 +10,13 @@ dependencies = [
|
||||
"openpyxl>=3.1.0",
|
||||
"click>=8.0.0"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
custombingo = "main:main"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# CustomBingo package
|
||||
@@ -0,0 +1,63 @@
|
||||
import random
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
|
||||
def generate_single_card(data: Dict[str, List[str]]) -> List[List[str]]:
|
||||
"""
|
||||
Generate a single BINGO card from the provided data.
|
||||
|
||||
Args:
|
||||
data: Dictionary with keys 'B', 'I', 'N', 'G', 'O' and values as lists of strings
|
||||
|
||||
Returns:
|
||||
5x5 matrix representing a BINGO card (list of lists)
|
||||
"""
|
||||
card = []
|
||||
|
||||
# Column B: numbers 1-15
|
||||
b_values = random.sample(data['B'], min(len(data['B']), 5))
|
||||
card.append(b_values + [''] * max(0, 5 - len(b_values)))
|
||||
|
||||
# Column I: numbers 16-30
|
||||
i_values = random.sample(data['I'], min(len(data['I']), 5))
|
||||
card.append(i_values + [''] * max(0, 5 - len(i_values)))
|
||||
|
||||
# Column N: numbers 31-45 (with free space in middle)
|
||||
n_values = random.sample(data['N'], min(len(data['N']), 5))
|
||||
# Insert empty string (free space) at position 2 (middle of column)
|
||||
if len(n_values) >= 3:
|
||||
n_values.insert(2, "FREE")
|
||||
else:
|
||||
n_values.append("FREE")
|
||||
card.append(n_values + [''] * max(0, 5 - len(n_values)))
|
||||
|
||||
# Column G: numbers 46-60
|
||||
g_values = random.sample(data['G'], min(len(data['G']), 5))
|
||||
card.append(g_values + [''] * max(0, 5 - len(g_values)))
|
||||
|
||||
# Column O: numbers 61-75
|
||||
o_values = random.sample(data['O'], min(len(data['O']), 5))
|
||||
card.append(o_values + [''] * max(0, 5 - len(o_values)))
|
||||
|
||||
# Transpose to get rows instead of columns
|
||||
transposed_card = [[card[col][row] for col in range(5)] for row in range(5)]
|
||||
|
||||
return transposed_card
|
||||
|
||||
|
||||
def generate_bingo_cards(data: Dict[str, List[str]], number_of_cards: int) -> List[List[List[str]]]:
|
||||
"""
|
||||
Generate multiple BINGO cards from the provided data.
|
||||
|
||||
Args:
|
||||
data: Dictionary with keys 'B', 'I', 'N', 'G', 'O' and values as lists of strings
|
||||
number_of_cards: Number of cards to generate
|
||||
|
||||
Returns:
|
||||
List of 5x5 matrices representing BINGO cards
|
||||
"""
|
||||
cards = []
|
||||
for _ in range(number_of_cards):
|
||||
card = generate_single_card(data)
|
||||
cards.append(card)
|
||||
return cards
|
||||
@@ -0,0 +1,131 @@
|
||||
from reportlab.lib.pagesizes import letter, A4
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from typing import List
|
||||
import math
|
||||
|
||||
|
||||
def calculate_font_size(text: str, available_width: float, available_height: float, max_font_size: int = 24) -> int:
|
||||
"""
|
||||
Calculate the largest font size that allows the text to fit in the given space.
|
||||
|
||||
Args:
|
||||
text: Text to fit
|
||||
available_width: Available width in points
|
||||
available_height: Available height in points
|
||||
max_font_size: Maximum font size to consider
|
||||
|
||||
Returns:
|
||||
Font size that allows the text to fit in the space
|
||||
"""
|
||||
if not text:
|
||||
return max_font_size
|
||||
|
||||
# Start with the maximum font size and decrease until text fits
|
||||
for font_size in range(max_font_size, 0, -1):
|
||||
# Estimate text dimensions (this is a rough approximation)
|
||||
# For more accuracy, we'd need to use the actual font metrics
|
||||
text_width = len(text) * font_size * 0.6 # rough character width estimate
|
||||
text_height = font_size
|
||||
|
||||
if text_width <= available_width and text_height <= available_height:
|
||||
return font_size
|
||||
|
||||
return 1 # smallest possible font size
|
||||
|
||||
|
||||
def draw_centered_text(c: canvas.Canvas, text: str, x: float, y: float, width: float, height: float, max_font_size: int = 24):
|
||||
"""
|
||||
Draw text centered in a given rectangle with the largest possible font size.
|
||||
|
||||
Args:
|
||||
c: ReportLab canvas
|
||||
text: Text to draw
|
||||
x: X coordinate of bottom-left of rectangle
|
||||
y: Y coordinate of bottom-left of rectangle
|
||||
width: Width of rectangle
|
||||
height: Height of rectangle
|
||||
max_font_size: Maximum font size to use
|
||||
"""
|
||||
if not text:
|
||||
return
|
||||
|
||||
font_size = calculate_font_size(text, width, height, max_font_size)
|
||||
c.setFont("Helvetica", font_size)
|
||||
|
||||
# Calculate the text dimensions
|
||||
text_width = c.stringWidth(text, "Helvetica", font_size)
|
||||
text_height = font_size
|
||||
|
||||
# Calculate centered position
|
||||
centered_x = x + (width - text_width) / 2
|
||||
# Vertically center the text
|
||||
# Using font_size * 0.8 as an approximation of text height
|
||||
centered_y = y + (height - text_height * 0.8) / 2 + text_height * 0.2
|
||||
|
||||
c.drawString(centered_x, centered_y, text)
|
||||
|
||||
|
||||
def export_cards_to_pdf(cards: List[List[List[str]]], output_file: str):
|
||||
"""
|
||||
Export the generated BINGO cards to a PDF file.
|
||||
|
||||
Args:
|
||||
cards: List of 5x5 matrices representing BINGO cards
|
||||
output_file: Path for the output PDF file
|
||||
"""
|
||||
# Create a new PDF document
|
||||
c = canvas.Canvas(output_file, pagesize=letter)
|
||||
width, height = letter
|
||||
|
||||
# Define card dimensions
|
||||
margin = 1 * inch
|
||||
card_width = (width - 2 * margin) * 0.8 # Make card 80% of page width
|
||||
card_height = card_width # Keep card square
|
||||
cell_width = card_width / 5
|
||||
cell_height = card_height / 5
|
||||
|
||||
# Starting position for the first card (centered horizontally)
|
||||
start_x = (width - card_width) / 2
|
||||
start_y = height - margin - card_height
|
||||
|
||||
for idx, card in enumerate(cards):
|
||||
# Add a new page for each card after the first
|
||||
if idx > 0:
|
||||
c.showPage()
|
||||
start_y = height - margin - card_height
|
||||
|
||||
# Draw title
|
||||
c.setFont("Helvetica", 24)
|
||||
title_width = c.stringWidth("BINGO", "Helvetica", 24)
|
||||
c.drawString((width - title_width) / 2, height - margin + 0.5 * inch, "BINGO")
|
||||
|
||||
# Draw the card grid
|
||||
for row in range(5):
|
||||
for col in range(5):
|
||||
# Calculate the position of this cell
|
||||
cell_x = start_x + col * cell_width
|
||||
cell_y = start_y + (4 - row) * cell_height # Flip Y-axis so row 0 is at bottom
|
||||
|
||||
# Draw rectangle for cell
|
||||
c.rect(cell_x, cell_y, cell_width, cell_height)
|
||||
|
||||
# Get the text for this cell
|
||||
cell_text = card[row][col]
|
||||
|
||||
# Draw the text centered in the cell
|
||||
draw_centered_text(c, cell_text, cell_x, cell_y, cell_width, cell_height)
|
||||
|
||||
# Draw column headers (B, I, N, G, O)
|
||||
headers = ['B', 'I', 'N', 'G', 'O']
|
||||
for col, header in enumerate(headers):
|
||||
cell_x = start_x + col * cell_width
|
||||
cell_y = start_y + 5 * cell_height # Above the grid
|
||||
|
||||
# Draw header cell (no border, just text)
|
||||
draw_centered_text(c, header, cell_x, cell_y, cell_width, cell_height, max_font_size=18)
|
||||
|
||||
# Save the PDF
|
||||
c.save()
|
||||
@@ -0,0 +1,44 @@
|
||||
import pandas as pd
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
def read_spreadsheet(file_path: str) -> Dict[str, List[str]]:
|
||||
"""
|
||||
Read a spreadsheet file and return the data organized by BINGO columns.
|
||||
|
||||
Args:
|
||||
file_path: Path to the Excel or CSV file
|
||||
|
||||
Returns:
|
||||
Dictionary with keys 'B', 'I', 'N', 'G', 'O' and values as lists of strings
|
||||
"""
|
||||
# Determine file type and read accordingly
|
||||
if file_path.lower().endswith('.csv'):
|
||||
df = pd.read_csv(file_path)
|
||||
elif file_path.lower().endswith(('.xlsx', '.xls')):
|
||||
df = pd.read_excel(file_path)
|
||||
else:
|
||||
raise ValueError("Unsupported file format. Please use CSV or Excel files.")
|
||||
|
||||
# Validate that the dataframe has exactly 5 columns
|
||||
if len(df.columns) != 5:
|
||||
raise ValueError(f"Spreadsheet must have exactly 5 columns, but found {len(df.columns)}")
|
||||
|
||||
# Assign column names if they're not already named B, I, N, G, O
|
||||
if not all(col in ['B', 'I', 'N', 'G', 'O'] for col in df.columns):
|
||||
df.columns = ['B', 'I', 'N', 'G', 'O']
|
||||
else:
|
||||
# Ensure the order is B, I, N, G, O
|
||||
df = df[['B', 'I', 'N', 'G', 'O']]
|
||||
|
||||
# Convert to dictionary of lists
|
||||
data = {}
|
||||
for col in ['B', 'I', 'N', 'G', 'O']:
|
||||
# Drop NaN values and convert to list of strings
|
||||
data[col] = df[col].dropna().astype(str).tolist()
|
||||
|
||||
# Check for empty columns
|
||||
if len(data[col]) == 0:
|
||||
raise ValueError(f"Column {col} is empty. Each column must contain at least one value.")
|
||||
|
||||
return data
|
||||
Reference in New Issue
Block a user