js根据开始结束日期进行月度分段?

作者站长头像
站长
· 阅读数 4

比如用户选择开始日期2022-01-15和结束日期2022-10-20,然后按月度分段,则会返回分段后的日期范围:

[
{startDate:2022-01-15, endDate:2022-01-31},
{startDate:2022-02-01, endDate:2022-02-28},
{startDate:2022-03-01, endDate:2022-03-31},
// ......
{startDate:2022-10-01, endDate:2022-10-20},
]
回复
1个回答
avatar
test
2024-07-10
// 把日期(按月)转换成整数,方便递增计算
function toNumber(date) {
    const d = new Date(date);
    return d.getFullYear() * 12 + d.getMonth();
}

// 把日期格式成 YYYY-MM-DD 格式
function format(date) {
    return [
        date.getFullYear(),
        date.getMonth() + 1,
        date.getDate()
    ].map(n => `${n}`.padStart(2, "0")).join("-");
}

function getDateSegments(begin, end) {
    const mBegin = toNumber(begin);     // 起月的整数表示
    const mEnd = toNumber(end);         // 止月的整数表示

    // 第 1 种情况,两个日期在同一个月,只有一段
    if (mBegin === mEnd) {
        return [{ startDate: begin, endDate: end }];
    }

    // 其它情况可以认为有 3 段
    // 起始日期所在月
    // (可能为空)其他月
    // 结束日期所在月

    // 先算起始日期所在月 prefix
    const prefix = (d => {
        const e = new Date(d.getTime());
        e.setDate(0);
        e.setMonth(e.getMonth() + 1);
        return { startDate: format(d), endDate: format(e) };
    })(new Date(begin));

    // 再算结束日期所在月 suffix
    const suffix = (d => {
        const b = new Date(d.getTime());
        b.setDate(1);
        return { startDate: format(b), endDate: format(d) };
    })(new Date(end));

    // 最后中间那部分(有可能是空的)
    const baseMonth = mBegin + 1;

    // 下面这句包含 from 中的 mapper 一共有 3 个 map 操作,可以合并
    // 这里为了清晰逻辑,分开写的,性能会略差一些
    const middle = Array
        .from(
            { length: mEnd - baseMonth },
            (_, i) => baseMonth + i
        )
        .map(m => ({ year: ~~(m / 12), month: m % 12 }))
        .map(({ year, month }) => ({
            startDate: format(new Date(year, month, 1)),
            endDate: format(new Date(year, month + 1, 0))
        }));

    // 三部分拼起来就是结果
    return [prefix, ...middle, suffix];
}

console.log(getDateSegments("2022-01-31", "2022-02-01"));
console.log("-------------");
console.log(getDateSegments("2022-02-01", "2022-02-01"));
console.log("-------------");
console.log(getDateSegments("2022-01-15", "2022-10-20"));

处理了一下,看起来要代码要短一些
function getDateSegments(begin, end) {
    Date.prototype.format = function () {
        return [this.getFullYear(), this.getMonth() + 1, this.getDate()]
            .map(n => `${n}`.padStart(2, "0")).join("-");
    };

    const mBegin = (d => d.getFullYear() * 12 + d.getMonth())(new Date(begin));
    const mEnd = (d => d.getFullYear() * 12 + d.getMonth())(new Date(end));

    // 第 1 种情况,两个日期在同一个月,只有一段
    if (mBegin === mEnd) {
        return [{
            startDate: new Date(begin).format(),
            endDate: new Date(end).format()
        }];
    }

    const prefix = (b => ({
        startDate: b.format(),
        endDate: new Date(b.getFullYear(), b.getMonth() + 1, 0).format()
    }))(new Date(begin));

    const suffix = (e => ({
        startDate: new Date(e.getFullYear(), e.getMonth(), 1).format(),
        endDate: e.format()
    }))(new Date(end));

    // 最后中间那部分(有可能是空的)

    function* middles() {
        for (let i = mBegin + 1; i < mEnd; i++) {
            const [y, m] = [~~(i / 12), i % 12];
            yield {
                startDate: new Date(y, m, 1).format(),
                endDate: new Date(y, m + 1, 0).format()
            };
        }
    }

    return [prefix, ...middles(), suffix];
}

之所以不用 toISOString().slice(0, 10) 要自己写 format,是因为下图的原因

answer image


2023-02-13 补充

用迭代的办法确实代码要短好多

function getDateSegments(begin, end) {
    Date.prototype.format = function () {
        return [this.getFullYear(), this.getMonth() + 1, this.getDate()]
            .map(n => `${n}`.padStart(2, "0")).join("-");
    };

    function* iterate(begin, end) {
        // mBegin 要和 end 比较,要保证他们是在同一个时区下进行比较,所以重新生成
        // 原因见下图
        end = new Date(end.getFullYear(), end.getMonth(), end.getDate());
        let mBegin = new Date(begin.getFullYear(), begin.getMonth(), begin.getDate());
        while (mBegin <= end) {
            const mEnd = new Date(mBegin.getFullYear(), mBegin.getMonth() + 1, 0);
            if (mEnd >= end) {
                yield { startDate: mBegin.format(), endDate: end.format() };
                break;
            }

            yield { startDate: mBegin.format(), endDate: mEnd.format() };
            mBegin = new Date(mBegin.getFullYear(), mBegin.getMonth() + 1, 1);
        }
    }
    return [...iterate(new Date(begin), new Date(end))];
}

iterate 的 mBegin 和 end 之所以重新生成,一个是下图时区的原因。另一个,需要去除输入 Date 对象的时间部分(万一有呢)

answer image

回复
likes
适合作为回答的
  • 经过验证的有效解决办法
  • 自己的经验指引,对解决问题有帮助
  • 遵循 Markdown 语法排版,代码语义正确
不该作为回答的
  • 询问内容细节或回复楼层
  • 与题目无关的内容
  • “赞”“顶”“同问”“看手册”“解决了没”等毫无意义的内容