Files
billai/web/src/lib/services/analysis.ts

130 lines
3.4 KiB
TypeScript

import type { UIBill } from '$lib/models/bill';
import type { CategoryStat, MonthlyStat, DailyExpenseData, TotalStats, PieChartDataItem } from '$lib/types/analysis';
import { pieColors } from '$lib/constants/chart';
/**
* 计算分类统计
*/
export function calculateCategoryStats(records: UIBill[]): CategoryStat[] {
const stats = new Map<string, { expense: number; income: number; count: number }>();
for (const r of records) {
if (!stats.has(r.category)) {
stats.set(r.category, { expense: 0, income: 0, count: 0 });
}
const s = stats.get(r.category)!;
s.count++;
const amount = r.amount || 0;
if (r.incomeExpense === '支出') {
s.expense += amount;
} else {
s.income += amount;
}
}
return [...stats.entries()]
.map(([category, data]) => ({ category, ...data }))
.sort((a, b) => b.expense - a.expense);
}
/**
* 计算月度统计
*/
export function calculateMonthlyStats(records: UIBill[]): MonthlyStat[] {
const stats = new Map<string, { expense: number; income: number }>();
for (const r of records) {
const month = r.time.substring(0, 7); // YYYY-MM
if (!stats.has(month)) {
stats.set(month, { expense: 0, income: 0 });
}
const s = stats.get(month)!;
const amount = r.amount || 0;
if (r.incomeExpense === '支出') {
s.expense += amount;
} else {
s.income += amount;
}
}
return [...stats.entries()]
.map(([month, data]) => ({ month, ...data }))
.sort((a, b) => a.month.localeCompare(b.month));
}
/**
* 计算每日支出数据(用于面积图)
*/
export function calculateDailyExpenseData(records: UIBill[]): DailyExpenseData[] {
const stats = new Map<string, number>();
for (const r of records) {
if (r.incomeExpense !== '支出') continue;
const date = r.time.substring(0, 10); // YYYY-MM-DD
const amount = r.amount || 0;
stats.set(date, (stats.get(date) || 0) + amount);
}
return [...stats.entries()]
.map(([date, value]) => ({ date: new Date(date), value }))
.sort((a, b) => a.date.getTime() - b.date.getTime());
}
/**
* 计算总计统计
*/
export function calculateTotalStats(records: UIBill[]): TotalStats {
return {
expense: records
.filter(r => r.incomeExpense === '支出')
.reduce((sum, r) => sum + (r.amount || 0), 0),
income: records
.filter(r => r.incomeExpense === '收入')
.reduce((sum, r) => sum + (r.amount || 0), 0),
count: records.length,
};
}
/**
* 计算百分比
*/
export function getPercentage(value: number, total: number): number {
return total > 0 ? (value / total) * 100 : 0;
}
/**
* 计算饼状图数据
*/
export function calculatePieChartData(
categoryStats: CategoryStat[],
totalExpense: number
): PieChartDataItem[] {
return categoryStats
.filter(s => s.expense > 0)
.map((stat, i) => ({
category: stat.category,
value: stat.expense,
color: pieColors[i % pieColors.length],
percentage: getPercentage(stat.expense, totalExpense)
}));
}
/**
* 获取 Top N 支出记录
*/
export function getTopExpenses(records: UIBill[], n: number = 10): UIBill[] {
return records
.filter(r => r.incomeExpense === '支出')
.sort((a, b) => (b.amount || 0) - (a.amount || 0))
.slice(0, n);
}
/**
* 统计支出/收入笔数
*/
export function countByType(records: UIBill[], type: '支出' | '收入'): number {
return records.filter(r => r.incomeExpense === type).length;
}