Week Of Code 28

四道编程题解析
本文解析了四道经典编程题目,包括船运问题、异或运算、幸运数字及友谊价值等,采用不同算法如贪心、位运算和数位DP等解决实际问题。

A.Boat Trips(水)

题目大意:

n条旅游线路,每条旅游线路 pi人。现在有m条船,每条船装c个人,问是否这些船能满足所有的旅游线路?

题目分析:

就是判断 mc是否全部小于pi.太水了

#include <bits/stdc++.h>
using namespace std;

#define RE(x) freopen(x,"r",stdin)
#define WR(x) freopen(x,"w",stdout)
#define ms(x,y) memset(x,y,sizeof(x))
#define pb push_back
#define mp make_pair
#define INF 0x3f3f3f3f
#define eps 1e-8

typedef long long ll;
typedef vector<int> vi;
typedef pair<int,int> pi; 
typedef vector<ll> vl;

const int M = 1e9 + 7;
const double PI = acos(-1.0);

const int MAXN = 1e5;
const int MAXM = 1e6;

int main() {
    //RE("in.txt");WR("out.txt");
    int n,c,m,p;
    cin>>n>>c>>m;
    string s="Yes";
    while(n--) {
        cin>>p;
        if(c*m<p)
            s="No";
    }
    cout<<s<<endl;
}

B.The Great XOR(位运算)

题目大意:

输入x,求有多少个a满足:a  XOR  x>x,0<a<x.

题目分析:

首先把x写开成二进制形式,从低位开始考虑:

如果x的该位是0,那么只要a在这一位取1,比他低的位任取,就都能满足题意,如果x在该位是0,那么a只能取0,不可能找到解。

例如:
x=11001101

首先a至多取比x少一位的数,也就是七位数,其中a不可能是七位数、四位数、三位数、一位数(因为异或之后就会变小),但可是任意的六位数、五位数、二位数
这样就很简单了。需要注意数据范围超过int。

#include <bits/stdc++.h>
using namespace std;

#define RE(x) freopen(x,"r",stdin)
#define WR(x) freopen(x,"w",stdout)
#define ms(x,y) memset(x,y,sizeof(x))
#define pb push_back
#define mp make_pair
#define INF 0x3f3f3f3f
#define eps 1e-8

typedef long long ll;
typedef vector<int> vi;
typedef pair<int,int> pi;
typedef vector<ll> vl;

const int M = 1e9 + 7;
const double PI = acos(-1.0);

const int MAXN = 1e5;
const int MAXM = 1e6;

ll q,x;
int main() {
    cin>>q;
    while(q--) {
        cin>>x;
        ll ans=0;
        for(int i=0;i<35;i++) {
            ll t=(1ll<<i);
            if(t>=x)
                break;
            if(!(t&x))
                ans+=(1ll<<i);
        }
        cout<<ans<<endl;
    }
}

C.Lucky Number Eight(数位dp)

题目大意:

给你一个n位的由数字组成的字符串(n的范围到2e5),问有多少个不连续的子串可以被8整除?输出结果mod 1e9+7.

题目分析:

一看就是数位DP,一开始脑子里闪过的是小学奥数中判定被8整除的法则,就是看末三位数,但是这样的话数组就开成了2e5*1000*8B=1.6GB,,内存显然是不够用的。

看了题解知道,是根据模8值进行转移的,这样数组只需开到2e5*8*8B=12.8MB即可。

然后就是类似于背包的思想,记dp[i][j]表示前i位已经确定,当前子串模值为j的方法数,则dp[i][j]=dp[i+1][j]+dp[i+1][k],其中k表示放入第i+1位后的新模值,有点类似于背包的思想。

记忆化dfs一下,最后dp[0][0]1即为所求,因为这里面还包括一个空串,其模值为0。注意数据范围即可。

#include <bits/stdc++.h>
using namespace std;

#define RE(x) freopen(x,"r",stdin)
#define WR(x) freopen(x,"w",stdout)
#define ms(x,y) memset(x,y,sizeof(x))
#define pb push_back
#define mp make_pair
#define INF 0x3f3f3f3f
#define eps 1e-8

typedef long long ll;
typedef vector<int> vi;
typedef pair<int,int> pi;
typedef vector<ll> vl;

const int M = 1e9 + 7;
const double PI = acos(-1.0);

const int MAXN = 2e5;
const int MAXM = 1e6;

int n;
char s[MAXN+5];
ll dp[MAXN+5][10];

void dfs(int pos,int mod) {
    if(pos==n) {
        dp[pos][mod]=(mod==0);
        return;
    }
    else if(dp[pos][mod]!=-1)
        return;
    int nextmod=(mod*10+(s[pos]-'0'))%8;
    dfs(pos+1,mod);
    dfs(pos+1,nextmod);

    dp[pos][mod]=(dp[pos+1][mod]+dp[pos+1][nextmod])%M;
}
int main() {
    cin>>n;
    for(int i=0;i<n;i++)
        cin>>s[i];
    ms(dp,-1);
    dfs(0,0);
    cout<<dp[0][0]-1<<endl;
}

D. The Value of Friendship(贪心算法)

题目大意:

给你n个人,m组直接的朋友关系,定义total值为每一个人的朋友数(包括直接和间接)的和,求一种加边方式,使得每一步的total值的总和最大。

题目分析:

首先观察到,对于n个人的朋友圈,其total值就是n×(n1),从头开始加边就是1*2+2*3+3*4…+n*(n-1).完成n-1条边之后,再在这个子图上加边或者在其他朋友圈里操作,增加的都是n*(n+1).那么我们得到了贪心规律:
先统计最后每个朋友圈有几个人,然后按大小排序,先加朋友圈大的,加完n-1次之后加第二大的。。。以此类推,因为先加最大的其n*(n-1)值就大,这样每一步的total值就是最大的

#include <bits/stdc++.h>
using namespace std;

#define RE(x) freopen(x,"r",stdin)
#define WR(x) freopen(x,"w",stdout)
#define ms(x,y) memset(x,y,sizeof(x))
#define pb push_back
#define mp make_pair
#define INF 0x3f3f3f3f
#define eps 1e-8

#define IN "D:/water-questions/temp/input/in.txt"
#define OUT "D:/water-questions/temp/input/out.txt"

typedef long long ll;
typedef vector<int> vi;
typedef pair<int,int> pi;
typedef vector<ll> vl;

const int M = 1e9 + 7;
const double PI = acos(-1.0);

const int MAXN = 1e5;
const int MAXM = 1e6;

int q,n,m;
int f[MAXN+5],r[MAXN+5],sum[MAXN+5];
ll ans;
int find(int x) {
    return f[x]==x?x:f[x]=find(f[x]);
}
void un(int i,int j) {
    int x=find(i),y=find(j);
    if(x!=y) {
        if(r[x]<r[y]) {
            f[x]=y;
            sum[y]+=sum[x];
        }
        else {
            f[y]=x;
            sum[x]+=sum[y];
            if(r[x]==r[y])
                r[x]++;
        }
    }
}
int main() {
    cin>>q;
    while(q--) {
        cin>>n>>m;
        for(int i=1;i<=n;i++) {
            f[i]=i;
            r[i]=1;
            sum[i]=1;
        }
        for(int i=0;i<m;i++) {
            int u,v;
            cin>>u>>v;
            if(find(u)!=find(v))
                un(u,v);
        }
        vl cp;
        for(int i=1;i<=n;i++)
            if(f[i]==i)
                cp.pb(sum[i]);
        sort(cp.begin(),cp.end());

        ll ans=0;
        ll edge=0;
        for(int i=cp.size()-1;i>=0;i--) {
            edge+=(cp[i]-1);
            ll temp=0;
            for(int j=1;j<cp[i];j++)
                temp+=(j*(j+1ll));

            ans+=((m-edge)*cp[i]*(cp[i]-1)+temp);
        }
        cout<<ans<<endl;
    }
}
/** * Field number for {@code get} and {@code set} indicating the * era, e.g., AD or BC in the Julian calendar. This is a calendar-specific * value; see subclass documentation. * * @see GregorianCalendar#AD * @see GregorianCalendar#BC */ public static final int ERA = 0; /** * Field number for {@code get} and {@code set} indicating the * year. This is a calendar-specific value; see subclass documentation. */ public static final int YEAR = 1; /** * Field number for {@code get} and {@code set} indicating the * month. This is a calendar-specific value. The first month of * the year in the Gregorian and Julian calendars is * {@code JANUARY} which is 0; the last depends on the number * of months in a year. * * @see #JANUARY * @see #FEBRUARY * @see #MARCH * @see #APRIL * @see #MAY * @see #JUNE * @see #JULY * @see #AUGUST * @see #SEPTEMBER * @see #OCTOBER * @see #NOVEMBER * @see #DECEMBER * @see #UNDECIMBER */ public static final int MONTH = 2; /** * Field number for {@code get} and {@code set} indicating the * week number within the current year. The first week of the year, as * defined by {@code getFirstDayOfWeek()} and * {@code getMinimalDaysInFirstWeek()}, has value 1. Subclasses define * the value of {@code WEEK_OF_YEAR} for days before the first week of * the year. * * @see #getFirstDayOfWeek * @see #getMinimalDaysInFirstWeek */ public static final int WEEK_OF_YEAR = 3; /** * Field number for {@code get} and {@code set} indicating the * week number within the current month. The first week of the month, as * defined by {@code getFirstDayOfWeek()} and * {@code getMinimalDaysInFirstWeek()}, has value 1. Subclasses define * the value of {@code WEEK_OF_MONTH} for days before the first week of * the month. * * @see #getFirstDayOfWeek * @see #getMinimalDaysInFirstWeek */ public static final int WEEK_OF_MONTH = 4; /** * Field number for {@code get} and {@code set} indicating the * day of the month. This is a synonym for {@code DAY_OF_MONTH}. * The first day of the month has value 1. * * @see #DAY_OF_MONTH */ public static final int DATE = 5; /** * Field number for {@code get} and {@code set} indicating the * day of the month. This is a synonym for {@code DATE}. * The first day of the month has value 1. * * @see #DATE */ public static final int DAY_OF_MONTH = 5; /** * Field number for {@code get} and {@code set} indicating the day * number within the current year. The first day of the year has value 1. */ public static final int DAY_OF_YEAR = 6; 帮我用中文统计0-6个是什么
08-23
// index.js - 主逻辑入口 const fs = require("fs"); const moment = require("moment"); // 加载配置 const config = JSON.parse(fs.readFileSync("config.json", "utf8")); // 引入工具函数 const { getStream, writeFile, generateTotalStats, generateDailyErrorCodeStats, filterPlayIdWithSuccessPriority, generateErrorCodeDeviceCoverageCSV, generateDailyErrorCodeCoverageMatrix } = require("./func"); // 输出目录 if (!fs.existsSync("general_info")) { fs.mkdirSync("general_info", { recursive: true }); } if (!fs.existsSync("device_stats_by_category")) { fs.mkdirSync("device_stats_by_category", { recursive: true }); } // 辅助函数:添加 pullStreamDuration 字段 const pullStreamDuration = (item) => { return item.pullStreamDuration !== undefined ? item : { ...item, pullStreamDuration: 0 }; }; /** * 主任务执行器 */ async function startTask({ path: dirPath, outputName, itemHandler }) { let result = []; try { const files = fs.readdirSync(dirPath); const validFiles = files.filter((name) => name !== outputName && /\.csv$/i.test(name) && name.includes("sqllab")); for (const name of validFiles) { const dayItems = await getStream({ path: dirPath, name, itemHandler }, config); result.push(...dayItems); } if (result.length === 0) return; result.sort((a, b) => moment(a.etime) - moment(b.etime)); // 输出整体指标 writeFile(result, outputName, "general_info"); // 输出总请求成功率与错误码占比 generateTotalStats(result, "err_summary.csv", "general_info"); // 输出用户感知成功率与错误码占比 generateTotalStats(filterPlayIdWithSuccessPriority(result), "err_summary_user_impact.csv", "general_info"); // 输出所有错误码在不同设备中的占比 generateErrorCodeDeviceCoverageCSV(result, "err_device_cov.csv", "device_stats_by_category"); // 输出所有错误码在不同设备中的占比(日、周、月) generateDailyErrorCodeCoverageMatrix(result, "err_device_cov_daily_weekly_monthly.csv", "device_stats_by_category"); // 提取 除 '0' 外 出现次数最多的 Top 10 错误码 const countMap = {}; // 1. 统计所有 errorCode 频次(跳过 errorCode === '0') for (const item of result) { const code = item.errorCode; if (code === "0") continue; // 排除 errorCode 为 '0' 的项 countMap[code] = (countMap[code] || 0) + 1; } // 2. 按频次降序排序,取前 10 个错误码 const top10ErrorCodes = Object.entries(countMap) .sort(([, a], [, b]) => b - a) // 按频次降序 .slice(0, 10) .map(([code]) => code); // 只取错误码 top10ErrorCodes.push("0"); // 3. 过滤原始数据:只保留属于这 Top 10 错误码的数据和0的数据 const filteredResult = result.filter((item) => top10ErrorCodes.includes(item.errorCode)); // Top10错误按(日、周、月)统计 generateDailyErrorCodeStats(filteredResult, top10ErrorCodes, "err_daily_weekly_monthly.csv", "general_info"); } catch (err) { console.error("Task failed:", err); } } /** * 启动任务 */ function getAllTask() { startTask({ path: "./", outputName: "general_info.csv", itemHandler: pullStreamDuration, }); } getAllTask(); // func.js - 工具函数模块 const fs = require("fs"); const csv = require("csv-parser"); const fastcsv = require("fast-csv"); const moment = require("moment"); const path = require("path"); /** * 数据处理:解析 ep 字段,提取 playId/deviceId/errorCode/etime */ function dataHandler(row) { let epObj = {}; try { if (row.ep && typeof row.ep === "string") { epObj = JSON.parse(row.ep.trim()); } } catch (err) {} const ct = Number(row.ct); const etime = !isNaN(ct) && row.ct ? moment(ct).format("YYYY-MM-DD HH:mm:ss") : ""; return { playId: epObj.playId ?? "", deviceId: epObj.deviceId ?? "", eid: row.eid ?? "", errorCode: epObj.errorCode ? `${epObj.errorCode}` : "0", etime, }; } /** * 流式读取 CSV 文件并返回 Promise<Array> */ function getStream(file, config) { return new Promise((resolve, reject) => { const { path: dirPath, name, itemHandler } = file; const result = []; const url = path.join(dirPath, name); // 安全提取配置 const startTime = config.start_time ? moment(config.start_time, "YYYY-MM-DD HH:mm:ss") : null; const endTime = config.end_time ? moment(config.end_time, "YYYY-MM-DD HH:mm:ss") : null; const excludeErrorCodes = Array.isArray(config.exclude_error_codes) ? config.exclude_error_codes : []; const targetEidType = config.target_eid ? config.target_eid : "Playback.Video.StartPlay"; fs.createReadStream(url) .pipe(csv()) .on("data", (row) => { let item = dataHandler(row); if (itemHandler) item = itemHandler(item); if (!item.etime) return; const itemTime = moment(item.etime, "YYYY-MM-DD HH:mm:ss"); if (!itemTime.isValid()) return; if (startTime && itemTime.isBefore(startTime)) return; if (endTime && itemTime.isAfter(endTime)) return; if (excludeErrorCodes.includes(item.errorCode)) return; if (item.eid !== targetEidType) return; result.push(item); }) .on("end", () => { resolve(result); }) .on("error", reject); }); } /** * 写入 CSV 文件 */ function writeFile(data, filename, outputDir) { const filepath = path.join(outputDir, filename); const ws = fs.createWriteStream(filepath); fastcsv.write(data, { headers: true }).pipe(ws); ws.on("finish", () => {}); ws.on("error", (err) => { console.error(`Failed to write ${filename}:`, err); }); } /** * 生成总错误码统计(带百分比) */ function generateTotalStats(result, filename, outputDir) { const countMap = {}; let totalCount = 0; for (const item of result) { const code = item.errorCode; countMap[code] = (countMap[code] || 0) + 1; totalCount++; } const totalStats = Object.entries(countMap) .map(([errorCode, count]) => ({ errorCode, percentage: ((count / totalCount) * 100).toFixed(2) + "%", count, })) .sort((a, b) => b.count - a.count); writeFile(totalStats, filename, outputDir); } /** * 生成每日错误码分布趋势 */ function generateDailyErrorCodeStats(result, topErrorCodes = [], filename, outputDir) { function getDate(etime) { return etime.split(" ")[0]; } const dailyStats = {}; // Step 1: 收集每日各 errorCode 出现次数 for (const item of result) { const date = getDate(item.etime); const errorCode = item.errorCode; if (!date || !topErrorCodes.includes(errorCode)) continue; if (!dailyStats[date]) { dailyStats[date] = {}; } dailyStats[date][errorCode] = (dailyStats[date][errorCode] || 0) + 1; } // Step 2: 获取所有日期并排序 const sortedDates = Object.keys(dailyStats).sort(); const tableData = []; const WEEK_SIZE = 7; const MONTH_SIZE = 30; for (let i = 0; i < sortedDates.length; i++) { const date = sortedDates[i]; const counts = dailyStats[date] || {}; const dailyTotal = Object.values(counts).reduce((a, b) => a + b, 0); const row = { date }; // === 日维度统计 === topErrorCodes.forEach((code) => { const count = counts[code] || 0; if (count > 0) { const percentage = ((count / dailyTotal) * 100).toFixed(2); row[`${code}_count`] = count; row[`${code}_percentage`] = `${percentage}%`; } else { row[`${code}_count`] = ""; row[`${code}_percentage`] = ""; } }); row.total = dailyTotal; // 就放在日维度结尾 // === 周维度:是否为本周第一天?=== const isInFirstDayOfWeek = i % WEEK_SIZE === 0; if (isInFirstDayOfWeek) { const weekGroup = sortedDates.slice(i, i + WEEK_SIZE); const weeklyCount = {}; let weeklyTotal = 0; for (const d of weekGroup) { const dayData = dailyStats[d] || {}; for (const code of topErrorCodes) { const c = dayData[code] || 0; weeklyCount[code] = (weeklyCount[code] || 0) + c; weeklyTotal += c; } } topErrorCodes.forEach((code) => { const count = weeklyCount[code] || 0; const percentage = weeklyTotal > 0 ? ((count / weeklyTotal) * 100).toFixed(2) : 0; row[`${code}_weekly_count`] = count; row[`${code}_weekly_percentage`] = `${percentage}%`; }); row.weekly_total = weeklyTotal; } else { topErrorCodes.forEach((code) => { row[`${code}_weekly_count`] = ""; row[`${code}_weekly_percentage`] = ""; }); row.weekly_total = ""; } // === 月维度:是否为本月第一天?=== const isInFirstDayOfMonth = i % MONTH_SIZE === 0; if (isInFirstDayOfMonth) { const monthGroup = sortedDates.slice(i, i + MONTH_SIZE); const monthlyCount = {}; let monthlyTotal = 0; for (const d of monthGroup) { const dayData = dailyStats[d] || {}; for (const code of topErrorCodes) { const c = dayData[code] || 0; monthlyCount[code] = (monthlyCount[code] || 0) + c; monthlyTotal += c; } } topErrorCodes.forEach((code) => { const count = monthlyCount[code] || 0; const percentage = monthlyTotal > 0 ? ((count / monthlyTotal) * 100).toFixed(2) : 0; row[`${code}_monthly_count`] = count; row[`${code}_monthly_percentage`] = `${percentage}%`; }); row.monthly_total = monthlyTotal; } else { topErrorCodes.forEach((code) => { row[`${code}_monthly_count`] = ""; row[`${code}_monthly_percentage`] = ""; }); row.monthly_total = ""; } tableData.push(row); } // === 构建带分隔列的 headers(不包含 final_total)=== const headers = [ "date", // --- 日维度 --- ...topErrorCodes.flatMap((code) => [`${code}_count`, `${code}_percentage`]), "total", "", // 🔹 空列:日与周之间的分隔 // --- 周维度 --- ...topErrorCodes.flatMap((code) => [`${code}_weekly_count`, `${code}_weekly_percentage`]), "weekly_total", "", // 🔹 空列:周与月之间的分隔 // --- 月维度 --- ...topErrorCodes.flatMap((code) => [`${code}_monthly_count`, `${code}_monthly_percentage`]), "monthly_total", // ✅ 不再添加 final_total ]; // 手动写入 CSV,支持空列和精确顺序 const ws = fs.createWriteStream(path.join(outputDir, filename)); // 先写 header ws.write(headers.join(",") + "\n"); // 再逐行写数据 tableData.forEach((row) => { const line = [ row.date, // 日维度 ...topErrorCodes.flatMap((code) => [row[`${code}_count`] || "", row[`${code}_percentage`] || ""]), row.total, "", // 空列分隔 // 周维度 ...topErrorCodes.flatMap((code) => [row[`${code}_weekly_count`] || "", row[`${code}_weekly_percentage`] || ""]), row.weekly_total, "", // 空列分隔 // 月维度 ...topErrorCodes.flatMap((code) => [row[`${code}_monthly_count`] || "", row[`${code}_monthly_percentage`] || ""]), row.monthly_total, ] .map((field) => `"${field}"`) .join(","); ws.write(line + "\n"); }); ws.on("finish", () => { console.log("Successfully wrote dimension_errorcode_distribution.csv without final_total"); }); ws.on("error", (err) => { console.error("Failed to write file:", err); }); } function generateErrorCodeDeviceCoverageCSV(result, filename, outputDir) { const errorToDeviceSet = new Map(); const allDevicesSet = new Set(); // 1. 遍历数据,收集: // a) 每个 errorCode 对应的唯一 deviceId // b) 全局所有 deviceId(用于计算分母) for (const item of result) { const { errorCode, deviceId } = item; if (!errorCode || !deviceId) continue; // 收集全局设备 allDevicesSet.add(deviceId); // 收集 error -> device 映射 if (!errorToDeviceSet.has(errorCode)) { errorToDeviceSet.set(errorCode, new Set()); } errorToDeviceSet.get(errorCode).add(deviceId); } const totalUniqueDevices = allDevicesSet.size; if (totalUniqueDevices === 0) { console.warn("No valid devices found in data."); return; } // 2. 构建结果:覆盖率 = deviceCount / totalUniqueDevices const coverageStats = []; for (const [errorCode, deviceSet] of errorToDeviceSet) { const deviceCount = deviceSet.size; const coverageRate = deviceCount / totalUniqueDevices; const percentageStr = (coverageRate * 100).toFixed(2) + "%"; // 如 "45.23%" coverageStats.push({ errorCode, deviceCount, // 可选:保留原始计数 totalDevices: totalUniqueDevices, coverageRate: percentageStr, // 输出为百分比字符串 // coverageRateDecimal: coverageRate // 如果需要小数形式(0.4523),可额外加字段 }); } // 排序:按覆盖设备数降序 coverageStats.sort((a, b) => b.deviceCount - a.deviceCount); // 3. 写入 CSV 文件 const ws = fs.createWriteStream(path.join(outputDir, filename)); fastcsv.write(coverageStats, { headers: true }).pipe(ws); } function generateDailyErrorCodeCoverageMatrix(result, filename, outputDir) { const dailyDataMap = new Map(); // date -> { allDevices: Set, errorToDeviceSet: Map<code, Set<deviceId>> } // 1. 遍历数据,按日期分组并收集设备和错误覆盖情况 for (const item of result) { const { errorCode, deviceId, etime } = item; if (!errorCode || !deviceId || !etime) continue; const date = etime.split(" ")[0]; // 提取 YYYY-MM-DD if (!moment(date, "YYYY-MM-DD", true).isValid()) continue; if (!dailyDataMap.has(date)) { dailyDataMap.set(date, { allDevices: new Set(), errorToDeviceSet: new Map(), }); } const dayRecord = dailyDataMap.get(date); dayRecord.allDevices.add(deviceId); if (!dayRecord.errorToDeviceSet.has(errorCode)) { dayRecord.errorToDeviceSet.set(errorCode, new Set()); } dayRecord.errorToDeviceSet.get(errorCode).add(deviceId); } // 2. 收集所有唯一 errorCodes(用于表头),去掉空码和"0" const allErrorCodes = [...dailyDataMap.values()].flatMap((day) => Array.from(day.errorToDeviceSet.keys())).filter((code) => code && code !== "0"); const uniqueErrorCodes = [...new Set(allErrorCodes)].sort(); // 3. 构建基础表格数据,并按日期升序排列 const tableData = []; const sortedDates = [...dailyDataMap.keys()].sort((a, b) => moment(a) - moment(b)); for (const date of sortedDates) { const dayRecord = dailyDataMap.get(date); const totalDevices = dayRecord.allDevices.size; const rowData = { date, totalDevices }; // --- 单日错误码覆盖 --- for (const errorCode of uniqueErrorCodes) { const coveredDevices = dayRecord.errorToDeviceSet.get(errorCode)?.size || 0; rowData[errorCode] = coveredDevices > 0 ? String(coveredDevices) : ""; } // --- 初始化 7天 聚合字段 --- rowData["totalDevices_7d"] = ""; for (const errorCode of uniqueErrorCodes) { rowData[`${errorCode}_7d`] = ""; } // --- 初始化 30天 聚合字段 --- rowData["totalDevices_30d"] = ""; for (const errorCode of uniqueErrorCodes) { rowData[`${errorCode}_30d`] = ""; } tableData.push(rowData); } // 4. 每7天聚合一次(从第0天开始,连续7天为一组) const groupSize7 = 7; for (let i = 0; i < tableData.length; i += groupSize7) { const weekGroup = tableData.slice(i, i + groupSize7); const startDate = weekGroup[0].date; const deviceUnion = new Set(); // 统计7天内的总设备数 const cumulativeCoverage = new Map(); // errorCode -> Set<deviceId> for (const row of weekGroup) { const date = row.date; const dayRecord = dailyDataMap.get(date); if (!dayRecord) continue; // 合并设备 for (const deviceId of dayRecord.allDevices) { deviceUnion.add(deviceId); } // 合并错误码覆盖 for (const [errorCode, deviceSet] of dayRecord.errorToDeviceSet) { if (!cumulativeCoverage.has(errorCode)) { cumulativeCoverage.set(errorCode, new Set()); } for (const dev of deviceSet) { cumulativeCoverage.get(errorCode).add(dev); } } } const firstDayRow = tableData.find((r) => r.date === startDate); if (firstDayRow) { firstDayRow["totalDevices_7d"] = String(deviceUnion.size); for (const errorCode of uniqueErrorCodes) { const count = cumulativeCoverage.get(errorCode)?.size || 0; firstDayRow[`${errorCode}_7d`] = count > 0 ? String(count) : ""; } } } // 5. 每30天聚合一次 const groupSize30 = 30; for (let i = 0; i < tableData.length; i += groupSize30) { const monthGroup = tableData.slice(i, i + groupSize30); const startDate = monthGroup[0].date; const deviceUnion = new Set(); // 30天内所有设备(去重) const cumulativeCoverage = new Map(); // errorCode -> Set<deviceId> for (const row of monthGroup) { const date = row.date; const dayRecord = dailyDataMap.get(date); if (!dayRecord) continue; // 合并设备 for (const deviceId of dayRecord.allDevices) { deviceUnion.add(deviceId); } // 合并错误码 for (const [errorCode, deviceSet] of dayRecord.errorToDeviceSet) { if (!cumulativeCoverage.has(errorCode)) { cumulativeCoverage.set(errorCode, new Set()); } for (const dev of deviceSet) { cumulativeCoverage.get(errorCode).add(dev); } } } const firstDayRow = tableData.find((r) => r.date === startDate); if (firstDayRow) { firstDayRow["totalDevices_30d"] = String(deviceUnion.size); for (const errorCode of uniqueErrorCodes) { const count = cumulativeCoverage.get(errorCode)?.size || 0; firstDayRow[`${errorCode}_30d`] = count > 0 ? String(count) : ""; } } } // 6. 构造最终表头:单日 | 空列 | 7天聚合 | 空列 | 30天聚合 const headers = [ "date", ...uniqueErrorCodes, "totalDevices", "-", // 分隔列 ...uniqueErrorCodes.map((code) => `${code}_7d`), "totalDevices_7d", "-", // 分隔列 ...uniqueErrorCodes.map((code) => `${code}_30d`), "totalDevices_30d", ]; // 7. 写出 CSV 文件 const ws = fs.createWriteStream(path.join(outputDir, filename)); // 在输出时处理分隔列:填充 "-" const dataWithSeparators = tableData.map((row) => { const newRow = { ...row }; newRow["-"] = ""; // 所有分隔列为空值 return newRow; }); fastcsv.write(dataWithSeparators, { headers }).pipe(ws); } /** * 对 playId 去重:优先保留 errorCode=0;否则保留最后一条 * 空 playId 被视为各自独立,全部保留 */ function filterPlayIdWithSuccessPriority(result) { const map = new Map(); // 存储按 playId 分组的结果(仅非空 playId) const emptyPlayIdItems = []; // 保存 playId 为空的条目,全部保留 for (const item of result) { const { playId, errorCode, etime } = item; if (!playId) { // ✅ 空 playId:直接加入独立数组,不参与任何去重 emptyPlayIdItems.push(item); continue; } const existing = map.get(playId); const isCurrentSuccess = String(errorCode).trim() === "0"; const isExistingSuccess = existing && String(existing.errorCode).trim() === "0"; if (!existing) { map.set(playId, item); } else if (isCurrentSuccess) { // 当前是成功的,优先保留 map.set(playId, item); } else if (!isExistingSuccess) { // 已存在的也不是成功的,则按时间保留更晚的 if (etime && existing.etime && new Date(etime) > new Date(existing.etime)) { map.set(playId, item); } } // 如果 existing 是 success 而当前不是,则跳过(不更新) } // 合并结果:非空 playId 的去重结果 + 所有空 playId 条目 const filteredByPlayId = Array.from(map.values()); return [...filteredByPlayId, ...emptyPlayIdItems].sort((a, b) => new Date(a.etime) - new Date(b.etime)); } // 导出所有函数 module.exports = { getStream, writeFile, generateTotalStats, generateDailyErrorCodeStats, filterPlayIdWithSuccessPriority, generateErrorCodeDeviceCoverageCSV, generateDailyErrorCodeCoverageMatrix, }; 能否实现不导出多个csv,而是一个csv中的多个sheet
最新发布
09-26
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值