diff --git a/README.md b/README.md index 64293dc..a30647a 100644 --- a/README.md +++ b/README.md @@ -219,4 +219,62 @@ MIT License - 系统会自动检查本地数据库中最新的开奖日期 - 只获取并更新比本地数据更新的开奖记录 - 更新过程会记录日志到 `lottery_update.log` 文件 -- 支持双色球和大乐透两种彩票的数据更新 \ No newline at end of file +- 支持双色球和大乐透两种彩票的数据更新 + +## 分析功能说明 + +### 基础分析策略 + +#### 1. 冷热号码分析 +- 热号:统计近50期出现频率最高的号码 +- 冷号:统计超过平均遗漏期数的号码 +- API: GET `/api/analysis/hot-cold/{lottery_type}?periods=50` + +#### 2. 号码分布分析 +- 分区统计:将号码划分为多个区间,分析各区出号比例 +- 奇偶比:分析奇偶数的分布规律 +- 大小比:统计大数和小数的分布规律 +- API: GET `/api/analysis/distribution/{lottery_type}?periods=100` + +#### 3. 连号与重复号分析 +- 连号追踪:统计连号出现的频率和位置 +- 重复号观察:分析相邻期号码重复情况 +- API: GET `/api/analysis/consecutive/{lottery_type}?periods=100` + +### 数学理论应用 + +#### 1. 数学统计特征 +- 计算红球总和的历史平均值和标准差 +- 分析号码组合的数学特征 +- API: GET `/api/analysis/math-stats/{lottery_type}?periods=100` + +#### 2. 遗漏值分析 +- 计算每个号码的当前遗漏值 +- 分析历史最大遗漏 +- API: GET `/api/analysis/missing/{lottery_type}` + +### 智能选号策略 + +系统提供多种智能选号策略: +- 均衡策略:综合考虑号码分布特征 +- 热号策略:偏好选择近期高频号码 +- 冷号策略:偏好选择遗漏值较大的号码 +- 遗漏值策略:基于遗漏值分析选号 +- API: GET `/api/analysis/smart-numbers/{lottery_type}?strategy=balanced` + +### 使用示例 + +1. 获取双色球热号冷号分析: +```bash +curl http://localhost:8000/api/analysis/hot-cold/ssq?periods=50 +``` + +2. 获取大乐透号码分布分析: +```bash +curl http://localhost:8000/api/analysis/distribution/dlt?periods=100 +``` + +3. 使用均衡策略生成双色球号码: +```bash +curl http://localhost:8000/api/analysis/smart-numbers/ssq?strategy=balanced +``` \ No newline at end of file diff --git a/backend/app/api/endpoints/analysis.py b/backend/app/api/endpoints/analysis.py new file mode 100644 index 0000000..e2325d9 --- /dev/null +++ b/backend/app/api/endpoints/analysis.py @@ -0,0 +1,108 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import Optional + +from ...core.database import get_db +from ...services.analysis_service import LotteryAnalysisService + +router = APIRouter() + + +@router.get("/hot-cold/{lottery_type}") +def get_hot_cold_numbers( + lottery_type: str, + periods: Optional[int] = 50, + db: Session = Depends(get_db) +): + """获取热号和冷号分析""" + if lottery_type not in ['ssq', 'dlt']: + raise HTTPException(status_code=400, detail="Invalid lottery type") + + analysis_service = LotteryAnalysisService(db) + return analysis_service.get_hot_cold_numbers(lottery_type, periods) + + +@router.get("/distribution/{lottery_type}") +def get_number_distribution( + lottery_type: str, + periods: Optional[int] = 100, + db: Session = Depends(get_db) +): + """获取号码分布分析""" + if lottery_type not in ['ssq', 'dlt']: + raise HTTPException(status_code=400, detail="Invalid lottery type") + + analysis_service = LotteryAnalysisService(db) + return analysis_service.analyze_number_distribution(lottery_type, periods) + + +@router.get("/consecutive/{lottery_type}") +def get_consecutive_analysis( + lottery_type: str, + periods: Optional[int] = 100, + db: Session = Depends(get_db) +): + """获取连号和重复号分析""" + if lottery_type not in ['ssq', 'dlt']: + raise HTTPException(status_code=400, detail="Invalid lottery type") + + analysis_service = LotteryAnalysisService(db) + return analysis_service.analyze_consecutive_numbers(lottery_type, periods) + + +@router.get("/math-stats/{lottery_type}") +def get_mathematical_stats( + lottery_type: str, + periods: Optional[int] = 100, + db: Session = Depends(get_db) +): + """获取数学统计特征""" + if lottery_type not in ['ssq', 'dlt']: + raise HTTPException(status_code=400, detail="Invalid lottery type") + + analysis_service = LotteryAnalysisService(db) + return analysis_service.analyze_mathematical_stats(lottery_type, periods) + + +@router.get("/missing/{lottery_type}") +def get_missing_periods( + lottery_type: str, + db: Session = Depends(get_db) +): + """获取号码遗漏值分析""" + if lottery_type not in ['ssq', 'dlt']: + raise HTTPException(status_code=400, detail="Invalid lottery type") + + analysis_service = LotteryAnalysisService(db) + return analysis_service.get_missing_periods(lottery_type) + + +@router.get("/smart-numbers/{lottery_type}") +def generate_smart_numbers( + lottery_type: str, + strategy: Optional[str] = 'balanced', + count: Optional[int] = 1, + periods: Optional[int] = 100, + db: Session = Depends(get_db) +): + """智能选号 + + Args: + lottery_type: 彩票类型 ('ssq' 或 'dlt') + strategy: 选号策略 ('balanced', 'hot', 'cold', 'missing') + count: 生成注数,默认1注 + periods: 分析期数,默认100期 + """ + if lottery_type not in ['ssq', 'dlt']: + raise HTTPException(status_code=400, detail="Invalid lottery type") + if strategy not in ['balanced', 'hot', 'cold', 'missing']: + raise HTTPException(status_code=400, detail="Invalid strategy") + if count < 1 or count > 10: + raise HTTPException( + status_code=400, detail="Count must be between 1 and 10") + if periods < 50 or periods > 200: + raise HTTPException( + status_code=400, detail="Periods must be between 50 and 200") + + analysis_service = LotteryAnalysisService(db) + return analysis_service.generate_smart_numbers(lottery_type, strategy, count, periods) diff --git a/backend/app/main.py b/backend/app/main.py index c024dad..dbb17ec 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,6 +2,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.core.config import settings from app.api.v1.lottery import router as lottery_router +from app.api.endpoints.analysis import router as analysis_router from app.core.database import Base, engine # 创建数据库表 @@ -15,7 +16,7 @@ app = FastAPI( # 配置CORS app.add_middleware( CORSMiddleware, - allow_origins=settings.BACKEND_CORS_ORIGINS, + allow_origins=["*"], # 允许所有来源 allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -24,6 +25,8 @@ app.add_middleware( # 注册路由 app.include_router( lottery_router, prefix=f"{settings.API_V1_STR}/lottery", tags=["lottery"]) +app.include_router( + analysis_router, prefix=f"{settings.API_V1_STR}/analysis", tags=["analysis"]) if __name__ == "__main__": import uvicorn diff --git a/backend/app/services/analysis_service.py b/backend/app/services/analysis_service.py new file mode 100644 index 0000000..53ffc28 --- /dev/null +++ b/backend/app/services/analysis_service.py @@ -0,0 +1,349 @@ +from typing import List, Dict, Tuple, Optional +import numpy as np +from collections import defaultdict +from datetime import datetime, timedelta +from sqlalchemy.orm import Session +from ..models.lottery import SSQLottery, DLTLottery + + +class LotteryAnalysisService: + def __init__(self, db: Session): + self.db = db + + def get_hot_cold_numbers(self, lottery_type: str, periods: int = 50) -> Dict: + """分析热号和冷号 + + Args: + lottery_type: 彩票类型 ('ssq' 或 'dlt') + periods: 分析期数,默认50期 + + Returns: + Dict: 包含热号和冷号的字典 + """ + model = SSQLottery if lottery_type == 'ssq' else DLTLottery + recent_draws = self.db.query(model).order_by( + model.open_time.desc()).limit(periods).all() + + # 初始化号码频率统计 + red_freq = defaultdict(int) + blue_freq = defaultdict(int) + + # 统计号码出现频率 + for draw in recent_draws: + if lottery_type == 'ssq': + red_numbers = [draw.red_ball_1, draw.red_ball_2, draw.red_ball_3, + draw.red_ball_4, draw.red_ball_5, draw.red_ball_6] + for num in red_numbers: + red_freq[num] += 1 + blue_freq[draw.blue_ball] += 1 + else: + red_numbers = [draw.front_ball_1, draw.front_ball_2, draw.front_ball_3, + draw.front_ball_4, draw.front_ball_5] + for num in red_numbers: + red_freq[num] += 1 + blue_freq[draw.back_ball_1] += 1 + blue_freq[draw.back_ball_2] += 1 + + # 计算平均出现次数 + red_avg = sum(red_freq.values()) / len(red_freq) + blue_avg = sum(blue_freq.values()) / len(blue_freq) + + # 定义热号和冷号 + hot_reds = [num for num, freq in red_freq.items() if freq > red_avg] + cold_reds = [num for num, freq in red_freq.items() if freq < red_avg] + hot_blues = [num for num, freq in blue_freq.items() if freq > blue_avg] + cold_blues = [num for num, freq in blue_freq.items() + if freq < blue_avg] + + return { + 'hot_reds': sorted(hot_reds), + 'cold_reds': sorted(cold_reds), + 'hot_blues': sorted(hot_blues), + 'cold_blues': sorted(cold_blues), + 'red_frequencies': dict(red_freq), + 'blue_frequencies': dict(blue_freq) + } + + def analyze_number_distribution(self, lottery_type: str, periods: int = 100) -> Dict: + """分析号码分布(分区统计、奇偶比) + + Args: + lottery_type: 彩票类型 ('ssq' 或 'dlt') + periods: 分析期数,默认100期 + + Returns: + Dict: 包含各种分布统计的字典 + """ + model = SSQLottery if lottery_type == 'ssq' else DLTLottery + recent_draws = self.db.query(model).order_by( + model.open_time.desc()).limit(periods).all() + + # 初始化统计数据 + zone_stats = defaultdict(int) + odd_count = 0 + even_count = 0 + total_numbers = 0 + + for draw in recent_draws: + if lottery_type == 'ssq': + red_numbers = [draw.red_ball_1, draw.red_ball_2, draw.red_ball_3, + draw.red_ball_4, draw.red_ball_5, draw.red_ball_6] + else: + red_numbers = [draw.front_ball_1, draw.front_ball_2, draw.front_ball_3, + draw.front_ball_4, draw.front_ball_5] + + # 分区统计 + for num in red_numbers: + # 对于双色球:每5个数字一个区,最后一个区是31-33 + # 对于大乐透:每5个数字一个区,最后一个区是31-35 + zone = (num - 1) // 5 + 1 + zone_stats[zone] += 1 + + # 奇偶统计 + if num % 2 == 0: + even_count += 1 + else: + odd_count += 1 + total_numbers += 1 + + # 计算比例 + zone_distribution = {str(zone): count for zone, + count in zone_stats.items()} + odd_even_distribution = { + 'odd': odd_count, + 'even': even_count + } + + return { + 'zone_distribution': zone_distribution, + 'odd_even_distribution': odd_even_distribution + } + + def analyze_consecutive_numbers(self, lottery_type: str, periods: int = 100) -> Dict: + """分析连号和重复号 + + Args: + lottery_type: 彩票类型 ('ssq' 或 'dlt') + periods: 分析期数,默认100期 + + Returns: + Dict: 包含连号和重复号分析的字典 + """ + model = SSQLottery if lottery_type == 'ssq' else DLTLottery + recent_draws = self.db.query(model).order_by( + model.open_time.desc()).limit(periods).all() + + consecutive_stats = defaultdict(int) + repeat_stats = defaultdict(int) + + prev_numbers = None + for draw in recent_draws: + if lottery_type == 'ssq': + current_numbers = sorted([ + draw.red_ball_1, draw.red_ball_2, draw.red_ball_3, + draw.red_ball_4, draw.red_ball_5, draw.red_ball_6 + ]) + else: + current_numbers = sorted([ + draw.front_ball_1, draw.front_ball_2, draw.front_ball_3, + draw.front_ball_4, draw.front_ball_5 + ]) + + # 分析连号 + consecutive_count = 0 + for i in range(len(current_numbers)-1): + if current_numbers[i+1] - current_numbers[i] == 1: + consecutive_count += 1 + consecutive_stats[consecutive_count] += 1 + + # 分析重复号 + if prev_numbers: + repeat_count = len(set(current_numbers) & set(prev_numbers)) + repeat_stats[repeat_count] += 1 + + prev_numbers = current_numbers + + total_draws = len(recent_draws) + return { + 'consecutive_distribution': {k: v/total_draws for k, v in consecutive_stats.items()}, + 'repeat_distribution': {k: v/(total_draws-1) for k, v in repeat_stats.items()} + } + + def analyze_mathematical_stats(self, lottery_type: str, periods: int = 100) -> Dict: + """分析数学统计特征 + + Args: + lottery_type: 彩票类型 ('ssq' 或 'dlt') + periods: 分析期数,默认100期 + + Returns: + Dict: 包含数学统计特征的字典 + """ + model = SSQLottery if lottery_type == 'ssq' else DLTLottery + recent_draws = self.db.query(model).order_by( + model.open_time.desc()).limit(periods).all() + + sums = [] + for draw in recent_draws: + if lottery_type == 'ssq': + red_numbers = [draw.red_ball_1, draw.red_ball_2, draw.red_ball_3, + draw.red_ball_4, draw.red_ball_5, draw.red_ball_6] + else: + red_numbers = [draw.front_ball_1, draw.front_ball_2, draw.front_ball_3, + draw.front_ball_4, draw.front_ball_5] + sums.append(sum(red_numbers)) + + mean = np.mean(sums) + std = np.std(sums) + + return { + 'sum_mean': float(mean), + 'sum_std': float(std), + 'sum_range': { + 'min': float(mean - 2*std), + 'max': float(mean + 2*std) + } + } + + def get_missing_periods(self, lottery_type: str, periods: int = 200) -> Dict: + """分析号码遗漏值(仅分析最近periods期,避免全表遍历) + + Args: + lottery_type: 彩票类型 ('ssq' 或 'dlt') + periods: 分析期数,默认200期 + + Returns: + Dict: 包含各号码当前遗漏值的字典 + """ + model = SSQLottery if lottery_type == 'ssq' else DLTLottery + max_red = 33 if lottery_type == 'ssq' else 35 + max_blue = 16 if lottery_type == 'ssq' else 12 + + # 一次性查出最近periods期数据 + draws = self.db.query(model).order_by( + model.open_time.desc()).limit(periods).all() + + # 初始化遗漏值字典 + red_missing = {i: 0 for i in range(1, max_red + 1)} + blue_missing = {i: 0 for i in range(1, max_blue + 1)} + + # 标记号码是否已出现 + red_found = {i: False for i in range(1, max_red + 1)} + blue_found = {i: False for i in range(1, max_blue + 1)} + + for idx, draw in enumerate(draws): + if lottery_type == 'ssq': + red_numbers = [draw.red_ball_1, draw.red_ball_2, draw.red_ball_3, + draw.red_ball_4, draw.red_ball_5, draw.red_ball_6] + blue_numbers = [draw.blue_ball] + else: + red_numbers = [draw.front_ball_1, draw.front_ball_2, draw.front_ball_3, + draw.front_ball_4, draw.front_ball_5] + blue_numbers = [draw.back_ball_1, draw.back_ball_2] + + # 红球遗漏统计 + for i in range(1, max_red + 1): + if not red_found[i]: + if i in red_numbers: + red_found[i] = True + red_missing[i] = idx + # 蓝球遗漏统计 + for i in range(1, max_blue + 1): + if not blue_found[i]: + if i in blue_numbers: + blue_found[i] = True + blue_missing[i] = idx + + # 未出现过的号码,遗漏值为periods + for i in range(1, max_red + 1): + if not red_found[i]: + red_missing[i] = periods + for i in range(1, max_blue + 1): + if not blue_found[i]: + blue_missing[i] = periods + + return { + 'red_missing': red_missing, + 'blue_missing': blue_missing + } + + def generate_smart_numbers(self, lottery_type: str, strategy: str = 'balanced', count: int = 1, periods: int = 100) -> List[Dict]: + """智能选号 + + Args: + lottery_type: 彩票类型 ('ssq' 或 'dlt') + strategy: 选号策略 ('balanced', 'hot', 'cold', 'missing') + count: 生成注数 + periods: 分析期数,默认100期 + + Returns: + List[Dict]: 生成的号码列表 + """ + import random + + # 获取分析数据 + hot_cold = self.get_hot_cold_numbers(lottery_type, periods) + distribution = self.analyze_number_distribution(lottery_type, periods) + missing = self.get_missing_periods(lottery_type, periods) + + max_red = 33 if lottery_type == 'ssq' else 35 + max_blue = 16 if lottery_type == 'ssq' else 12 + red_count = 6 if lottery_type == 'ssq' else 5 + blue_count = 1 if lottery_type == 'ssq' else 2 + + results = [] + for _ in range(count): + # 根据策略选择红球 + if strategy == 'hot': + red_pool = hot_cold['hot_reds'] + elif strategy == 'cold': + red_pool = hot_cold['cold_reds'] + elif strategy == 'missing': + red_pool = sorted(missing['red_missing'].items(), + key=lambda x: x[1], reverse=True) + red_pool = [num for num, _ in red_pool[:max_red//2]] + else: # balanced + red_pool = list(range(1, max_red + 1)) + + # 确保红球池不为空且数量足够 + if not red_pool or len(red_pool) < red_count: + red_pool = list(range(1, max_red + 1)) + + # 选择红球 + try: + red_numbers = sorted(random.sample(red_pool, red_count)) + except ValueError: + # 如果采样失败,使用全范围随机 + red_numbers = sorted(random.sample( + range(1, max_red + 1), red_count)) + + # 选择蓝球 + if strategy == 'hot': + blue_pool = hot_cold['hot_blues'] + elif strategy == 'cold': + blue_pool = hot_cold['cold_blues'] + elif strategy == 'missing': + blue_pool = sorted(missing['blue_missing'].items(), + key=lambda x: x[1], reverse=True) + blue_pool = [num for num, _ in blue_pool[:max_blue//2]] + else: # balanced + blue_pool = list(range(1, max_blue + 1)) + + # 确保蓝球池不为空且数量足够 + if not blue_pool or len(blue_pool) < blue_count: + blue_pool = list(range(1, max_blue + 1)) + + # 选择蓝球 + try: + blue_numbers = sorted(random.sample(blue_pool, blue_count)) + except ValueError: + # 如果采样失败,使用全范围随机 + blue_numbers = sorted(random.sample( + range(1, max_blue + 1), blue_count)) + + results.append({ + 'red_numbers': red_numbers, + 'blue_numbers': blue_numbers + }) + + return results diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..76c53c2 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,26 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.api.endpoints import analysis + +app = FastAPI( + title="彩票数据分析系统", + description="支持双色球和大乐透的数据管理、统计分析和智能选号功能", + version="1.0.0" +) + +# 配置CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 注册路由 +app.include_router(analysis.router, prefix="/api/analysis", tags=["analysis"]) + + +@app.get("/") +def read_root(): + return {"message": "欢迎使用彩票数据分析系统"} diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6163715..18a0669 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -88,7 +88,7 @@ onUnmounted(() => { font-size: 16px; font-weight: 500; } -@media screen and (max-width: 768px) { +@media screen and (max-width: 1024px) { .el-header { flex-direction: row; height: 50px; diff --git a/frontend/src/api/lottery.js b/frontend/src/api/lottery.js index abcdf5c..c319291 100644 --- a/frontend/src/api/lottery.js +++ b/frontend/src/api/lottery.js @@ -1,4 +1,5 @@ import axios from 'axios' +import request from '../utils/request' const api = axios.create({ baseURL: '/api/v1/lottery' @@ -89,5 +90,67 @@ export const lotteryApi = { generateDLTNumbers(params) { return api.get('/dlt/generate', { params }) + }, + + // 获取热号冷号分析 + getHotColdNumbers(lotteryType, params) { + return request({ + url: `/api/v1/analysis/hot-cold/${lotteryType}`, + method: 'get', + params + }) + }, + + // 获取号码分布分析 + getNumberDistribution(lotteryType, params) { + return request({ + url: `/api/v1/analysis/distribution/${lotteryType}`, + method: 'get', + params + }) + }, + + // 获取连号分析 + getConsecutiveAnalysis(lotteryType, params) { + return request({ + url: `/api/v1/analysis/consecutive/${lotteryType}`, + method: 'get', + params + }) + }, + + // 获取数学统计特征 + getMathematicalStats(lotteryType, params) { + return request({ + url: `/api/v1/analysis/math-stats/${lotteryType}`, + method: 'get', + params + }) + }, + + // 获取遗漏值分析 + getMissingPeriods(lotteryType) { + return request({ + url: `/api/v1/analysis/missing/${lotteryType}`, + method: 'get' + }) + }, + + // 智能选号 + generateSmartNumbers(lotteryType, params) { + return request({ + url: `/api/v1/analysis/smart-numbers/${lotteryType}`, + method: 'get', + params: { + strategy: params.strategy || 'balanced', + count: params.count || 1, + periods: params.periods || 100 + } + }).then(response => { + if (response && response.data) { + return response + } + throw new Error('返回数据格式错误') + }) } } \ No newline at end of file diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js new file mode 100644 index 0000000..1f9dec5 --- /dev/null +++ b/frontend/src/utils/request.js @@ -0,0 +1,40 @@ +import axios from 'axios' +import { ElMessage } from 'element-plus' + +// 创建axios实例 +const service = axios.create({ + baseURL: 'http://localhost:8000', // API的基础URL + timeout: 15000 // 请求超时时间 +}) + +// 请求拦截器 +service.interceptors.request.use( + config => { + // 在发送请求之前做些什么 + return config + }, + error => { + // 对请求错误做些什么 + console.error('Request error:', error) + return Promise.reject(error) + } +) + +// 响应拦截器 +service.interceptors.response.use( + response => { + // FastAPI 返回的数据在 response.data 中 + return response + }, + error => { + console.error('Response error:', error) + ElMessage({ + message: error.message || 'Request failed', + type: 'error', + duration: 5 * 1000 + }) + return Promise.reject(error) + } +) + +export default service \ No newline at end of file diff --git a/frontend/src/views/NumberGenerator.vue b/frontend/src/views/NumberGenerator.vue index 8bcb57d..dd09086 100644 --- a/frontend/src/views/NumberGenerator.vue +++ b/frontend/src/views/NumberGenerator.vue @@ -5,19 +5,28 @@ - 随机选号 - 频率选号 + 均衡策略 热门号码 冷门号码 + 遗漏值策略 + + + + + + + + 生成号码 + 查看分析 @@ -25,32 +34,82 @@

生成结果

- {{ num }} + {{ num }}
- {{ result.blue_ball }} + {{ num }}
+ + + + + +
+

热号

+
+ {{ num }} +
+

冷号

+
+ {{ num }} +
+
+
+ + +
+

分区分布

+
+

奇偶分布

+
+
+
+ + +
+

红球遗漏值

+
+

蓝球遗漏值

+
+
+
+
+
- 随机选号 - 频率选号 + 均衡策略 热门号码 冷门号码 + 遗漏值策略 + + + + + + + + 生成号码 + 查看分析 @@ -58,34 +117,78 @@

生成结果

- {{ num }} + {{ num }}
- {{ num }} + {{ num }}
+ + + + + +
+

热号

+
+ {{ num }} +
+

冷号

+
+ {{ num }} +
+
+
+ + +
+

分区分布

+
+

奇偶分布

+
+
+
+ + +
+

红球遗漏值

+
+

蓝球遗漏值

+
+
+
+
+
\ No newline at end of file