比较2个日期区间重合的天数

本文介绍了一个SQL函数,用于计算两个日期区间内的天数。通过比较两个日期范围的起始和结束日期,该函数可以精确地计算出它们之间的天数总数。

create function [dbo].[GetRangeDays] (@dtRange1From DATE,
      @dtRange1To DATE,
      @dtRange2From DATE,
      @dtRange2To DATE)
RETURNS int
    AS
    BEGIN
    
    IF NOT ( @dtRange2From > @dtRange1To OR @dtRange2To < @dtRange1From )
        BEGIN
    
            DECLARE @dt1 DATE
            DECLARE @dt2 DATE
    
            SELECT  @dt1 = @dtRange1From,
                    @dt2 = @dtRange1To
    
            IF @dtRange2From > @dt1
                SET @dt1 = @dtRange2From
    
            IF @dtRange2To < @dt2
                SET @dt2 = @dtRange2To
    
            RETURN DATEDIFF(d, @dt1, @dt2) + 1
    
        END
    
      RETURN 0
    END

GO

<think>根据需求,我们需要在React中使用ECharts绘制折线图,其中X轴的数据点是非均匀分布的,因为每个标签对应的时间间隔不同(周、月、年)。我们需要将时间转换为相对于当前日的具体日,然后使用这些日作为X轴的数据,同时显示对应的标签(如'sw','2w'等)。 步骤: 1. 计每个标签对应的具体日(相对于当前日)。 2. 将日转换为时间戳(或字符串)作为X轴的数据点,但注意ECharts需要数值类型的数据点来保证非均匀分布。 3. 使用时间戳(数值)作为X轴的数据,这样坐标点就会根据实际的时间间隔非均匀分布。 4. 设置X轴的刻度标签(axisLabel)为我们给定的标签数组。 但是,ECharts的X轴如果是类目轴(category)则默认是均匀分布的,所以我们需要使用数值轴(value)或时间轴(time)。这里我们使用时间轴(time)来更准确地表示日。 具体步骤: - 获取当前日。 - 根据每个标签计对应的日(例如'sw'表示当前日+1周,'2w'表示+2周,'1M'表示+1个月,等等)。 - 将计出的日转换为时间戳(毫秒数)作为X轴数据。 - 在ECharts配置中,设置X轴为时间轴(type: 'time'),并设置轴标签的格式化函数,使其显示为我们指定的标签(如'sw','2w'等),而不是日格式。 注意:由于我们要求显示的标签是给定的字符串,而不是日,所以我们需要自定义一个映射关系,在axisLabel的formatter中根据时间戳返回对应的标签字符串。 但是,问题在于我们如何将时间戳映射回标签?我们可以创建一个数组,其中每个元素是一个对象,包含时间戳(value)和标签(label)。然后在formatter中根据给定的时间戳查找对应的标签。 然而,由于时间戳是精确到毫秒的,我们可能无法精确匹配(因为计时可能会有微小误差,比如计1个月后的日时,可能因为月份天数不同而得到不同的时间)。因此,我们可以预先计每个标签的时间戳,并存储在一个数组中。然后在formatter中,我们遍历这个数组,找到与当前值最接近的时间戳(因为误差很小,我们可以认为在一定误差范围内匹配),然后返回对应的标签。 另一种更简单的方法:由于我们只有11个点,我们可以直接根据索引来返回标签。但是注意,时间轴是连续的,而我们的点是不均匀的,所以我们需要确保每个点的时间戳是准确的,然后在formatter中,我们只在我们预先计时间戳位置显示标签,其他位置不显示标签(或者显示空)。我们可以通过设置axisLabel的interval为0,并自定义formatter来实现。 然而,ECharts的时间轴默认会尝试均匀地显示标签,而我们希望只在数据点位置显示标签。我们可以使用axisLabel的formatter来控制:只在我们指定的时间戳位置显示标签,其他位置不显示。我们可以通过判断当前时间戳是否在我们存储的标签时间戳数组中(或接近)来决定显示什么。 具体实现思路: 1. 计每个标签对应的日对象,并存储为时间戳(毫秒数)。 2. 将时间戳数组作为X轴数据,数值数组作为Y轴数据。 3. 在X轴配置中,设置type为'time',并设置axisLabel的formatter为一个函数,该函数会检查当前值(时间戳)是否在我们预先计时间戳数组中(允许一定的误差),如果在,则返回对应的标签,否则返回空字符串。但是这样可能会因为误差导致无法匹配。 4. 为了避免误差问题,我们可以预先将每个点的时间戳和标签存储起来,然后在formatter中遍历这个数组,找到与当前值最接近的(比如差值小于一天)的时间戳,然后返回其标签。如果没有找到,就返回空字符串。 但是,由于我们的点很少,且每个点都是我们明确指定的,我们可以将时间戳数组作为查找表。注意:因为计时可能因为时区问题导致时间戳有差异,所以我们在计时最好使用UTC时间,避免时区问题。 另一种方案:我们使用类目轴(category)并设置每个点的位置?但是类目轴是均匀的,所以不行。 我们还可以使用数值轴(value)直接使用时间戳,然后通过axisLabel的formatter来显示标签,这样坐标点就会按照时间戳的间隔非均匀分布。 我决定采用时间轴(time)并自定义formatter。 步骤: 1. 计每个标签对应的日(使用JavaScript的Date对象)。 2. 将日转换为时间戳(毫秒数)。 3. 创建一个映射数组:每个元素为{ time: 时间戳, label: 标签 }。 4. 在ECharts配置中,series的data应该是一个二维数组,每个元素为[时间戳, 数值]。 5. 配置xAxis: type: 'time', axisLabel: { formatter: function (value) { // 在映射数组中找到与value最接近的项(允许一定误差),返回其label // 由于我们的点不会太多,所以可以遍历 const map = ...; // 我们的映射数组 for (let i = 0; i < map.length; i++) { // 因为时间戳可能存在计误差,我们允许误差在一天之内(86400000毫秒) if (Math.abs(value - map[i].time) < 86400000 / 2) { return map[i].label; } } return ''; // 没有匹配则返回空 } } 但是,这样可能会在非数据点位置也尝试显示标签(比如在数据点之间),而我们希望只在数据点位置显示标签。我们可以通过设置axisLabel.interval=0来强制每个刻度都显示,然后通过formatter来控制只显示我们想要的标签。但是时间轴的刻度是自动生成的,我们无法直接控制只在数据点位置显示标签。 另一种方法:我们设置min和max为第一个和最后一个时间戳,然后设置axisLabel的interval为0,并设置splitNumber为数据点个数-1,这样可能会在每个数据点位置显示标签。但是ECharts的时间轴刻度是自动计的,不一定与数据点重合。 因此,我们可以使用另一种方式:使用value轴,然后通过axisLabel的formatter来显示标签。但是这样我们需要自己计每个点的位置(用时间戳作为数值),然后设置xAxis为value轴,并设置axisLabel的formatter。 具体: xAxis: { type: 'value', axisLabel: { formatter: function (value) { // value是时间戳 // 同样在映射数组中查找 // 同上 } } } 但是这样,坐标轴会显示数值(时间戳),虽然我们通过formatter转换成了标签,但是坐标轴上的刻度线还是按照数值均匀分布?不对,我们使用value轴,那么坐标点会按照数值大小均匀分布,但是我们的数据点的时间戳不是均匀的,所以坐标点在图表上的位置就是非均匀的。这正是我们想要的。 所以,我们可以使用value轴,然后设置axisLabel的formatter来显示标签。同时,我们设置xAxis的min和max为第一个和最后一个数据点的时间戳,这样坐标轴范围就正好覆盖所有点。 步骤总结: 1. 计每个标签对应的日,并转换为时间戳,得到时间戳数组timestamps。 2. 将x轴数据设为timestamps,y轴数据不变。 3. 配置xAxis为value轴(type: 'value'),并设置min为第一个时间戳,max为最后一个时间戳(或者可以自动调整,但为了精确,我们可以设置)。 4. 在axisLabel的formatter中,根据时间戳值查找对应的标签。 但是,这样设置后,坐标轴上的刻度会自动生成,可能不会在每个数据点位置都有刻度。我们可以通过设置axisTick和axisLabel的interval为0,并强制在每个数据点位置显示标签吗? 我们可以设置splitLine为false,然后设置axisTick的alignWithLabel为true?但是value轴并没有这个选项。 另一种方法:我们设置xAxis的axisPointer为true,然后设置axisLabel的interval为0?这样可能会在每个数据点位置都显示标签吗?不一定。 我们可以通过设置min和max,以及splitNumber为数据点个数-1,这样刻度数量可能会接近数据点数量,但不一定精确。 更可靠的方法:我们设置xAxis的axisLabel的interval为0,然后通过min和max以及splitNumber来控制。但是splitNumber只是建议值,ECharts会自动调整。 因此,我们可以使用另一种思路:将数据点的时间戳作为xAxis的data,但是这样我们只能使用类目轴,而类目轴是均匀的,所以不行。 所以,我们回到时间轴(time)并自定义formatter,同时设置坐标轴的分割点为我们指定的时间戳数组。ECharts提供了设置坐标轴分割点的方法:通过axisTick和splitLine的配置,我们可以设置坐标轴刻度在数据点位置。但是时间轴没有直接设置分割点数组的选项。 因此,我们采用value轴,并设置axisLabel的formatter,同时设置xAxis的min和max,以及splitNumber为数据点个数-1,这样刻度数量大致等于数据点数量,然后我们在formatter中,如果当前刻度值不在我们的时间戳数组中,就返回空字符串,这样只有数据点位置的标签才会显示。 具体: xAxis: { type: 'value', min: timestamps[0], max: timestamps[timestamps.length - 1], splitNumber: timestamps.length - 1, axisLabel: { formatter: function (value) { // 在映射数组中找到精确匹配(因为splitNumber设置后,刻度值应该会接近我们的时间戳,但可能不完全相等,所以还是需要允许误差) const map = ...; // 映射数组 const found = map.find(item => Math.abs(item.time - value) < 86400000 / 2); if (found) { return found.label; } return ''; // 其他位置不显示标签 } } } 这样,坐标轴上就会显示多个刻度,但只有在我们数据点位置的刻度才会显示标签,其他位置的标签为空字符串,所以不会显示。同时,由于splitNumber设置为数据点个数-1,刻度数量应该和数据点数量一致,所以每个数据点位置都会有一个刻度。 但是,splitNumber只是建议值,ECharts可能会调整。因此,我们可以使用另一种方式:设置axisTick和axisLabel的interval为0,并设置splitLine为false,然后通过设置axisLabel的showMinLabel和showMaxLabel等,但是value轴没有这些。 经过权衡,我决定使用value轴,并设置min和max,然后通过设置splitNumber为数据点个数-1,这样应该能保证每个数据点位置都有刻度(因为splitNumber是刻度区间的段数,所以段数=数据点个数-1,则刻度数=数据点个数)。 代码实现: 步骤: 1. 计每个标签对应的日(相对于当前日)。 2. 将日转换为时间戳,并存储标签和时间戳的映射。 3. 准备两个数组:xData(时间戳数组)和yData(原始y轴数据)。 4. 在ECharts配置中,series的data使用xData和yData组合成二维数组:[[x0,y0], [x1,y1], ...]。 5. 配置xAxis为value轴,并设置min、max、splitNumber以及axisLabel的formatter。 注意:计时,要考虑月份和年份的加减。由于每个月的天数不同,所以使用setMonth和setFullYear等方法。 计函数: 当前日:const baseDate = new Date(); sw: baseDate + 1周 -> new Date(baseDate.getTime() + 7 * 24 * 60 * 60 * 1000) 2w: +14天 3w: +21天 1M: 将月份+1,注意如果月份为12,则年份+1,月份置为1。 2M: 月份+2,同样处理 ... 以此类推 但是,我们可以使用Date对象的setMonth方法,它会自动处理溢出。例如: let date = new Date(); date.setMonth(date.getMonth() + 1); // 加1个月 同样,年份:date.setFullYear(date.getFullYear() + 1) 因此,我们可以写一个函数来根据标签计。 标签数组:['sw', '2w', '3w', '1M', '2M', '3M', '4M', '5M', '6M', '9M', '1Y'] 计规则: sw: +1周 2w: +2周 3w: +3周 1M: +1个月 ... 以此类推 注意:'1Y'表示+1年。 实现: 我们将标签映射为要加的时间: sw: { unit: 'week', value: 1 } 2w: { unit: 'week', value: 2 } 3w: { unit: 'week', value: 3 } 1M: { unit: 'month', value: 1 } ... 以此类推 然后根据单位计。 但是,我们也可以直接写一个函数,根据标签字符串计。 具体代码: 由于标签字符串是固定的,我们可以这样: function getDateFromLabel(label, baseDate) { let date = new Date(baseDate); if (label.endsWith('w')) { let weeks = parseInt(label); if (isNaN(weeks)) { // 如果是'sw',则weeks=1 if (label === 'sw') { weeks = 1; } else { // 无法解析 return baseDate; } } date.setDate(date.getDate() + weeks * 7); } else if (label.endsWith('M')) { let months = parseInt(label); if (isNaN(months)) { return baseDate; } date.setMonth(date.getMonth() + months); } else if (label.endsWith('Y')) { let years = parseInt(label); if (isNaN(years)) { return baseDate; } date.setFullYear(date.getFullYear() + years); } return date; } 但是,注意标签'sw',它没有数字,所以单独处理。 但是,我们也可以将标签数组和对应的增量预先定义好: const labelMap = { 'sw': { weeks: 1 }, '2w': { weeks: 2 }, '3w': { weeks: 3 }, '1M': { months: 1 }, '2M': { months: 2 }, '3M': { months: 3 }, '4M': { months: 4 }, '5M': { months: 5 }, '6M': { months: 6 }, '9M': { months: 9 }, '1Y': { years: 1 } }; 然后根据这个映射计。 这样更清晰。 代码实现: 步骤: 1. 定义labelMap,将每个标签映射到增加的时间(单位:天、周、月、年)。 2. 获取当前日作为基准日。 3. 遍历标签数组,对于每个标签,根据labelMap中的定义计新的日。 4. 将新日转换为时间戳(毫秒数)。 5. 构建映射数组:{ time: 时间戳, label: 标签 }。 6. 构建echarts的series数据:对于每个标签,数据为[时间戳, 对应的y值]。 然后配置echarts: 注意:我们使用value轴作为x轴,并设置min为第一个时间戳,max为最后一个时间戳,splitNumber为标签数量-1(即10)。 设置xAxis.axisLabel.formatter,根据值查找映射数组中的标签。 但是,由于我们使用value轴,坐标轴上的刻度是数值(时间戳),我们希望只显示标签,所以通过formatter转换。 另外,我们可以设置坐标轴的分割线(splitLine)为不显示,因为非均匀分布的分割线可能会误导。 开始写代码: 我们将使用React和ECharts,所以需要安装echarts和echarts-for-react(或者直接使用echarts的React组件)。这里我们假设使用echarts-for-react。 安装: npm install echarts echarts-for-react 代码结构: import React from 'react'; import ReactECharts from 'echarts-for-react'; const MyChart = () => { // 基准日 const baseDate = new Date(); // 标签数组 const labels = ['sw', '2w', '3w', '1M', '2M', '3M', '4M', '5M', '6M', '9M', '1Y']; const yData = [2, 7, 3, 5, 10, 29, 3, 5, 6, 9, 10]; // 标签映射到增量 const labelMap = { 'sw': { weeks: 1 }, '2w': { weeks: 2 }, '3w': { weeks: 3 }, '1M': { months: 1 }, '2M': { months: 2 }, '3M': { months: 3 }, '4M': { months: 4 }, '5M': { months: 5 }, '6M': { months: 6 }, '9M': { months: 9 }, '1Y': { years: 1 } }; // 计每个标签对应的日时间戳 const dataPoints = labels.map((label, index) => { const offset = labelMap[label]; const date = new Date(baseDate); if (offset.weeks) { date.setDate(date.getDate() + offset.weeks * 7); } else if (offset.months) { date.setMonth(date.getMonth() + offset.months); } else if (offset.years) { date.setFullYear(date.getFullYear() + offset.years); } const timestamp = date.getTime(); return { time: timestamp, label: label, yValue: yData[index] }; }); // 提取时间戳数组和用于series的数据 const seriesData = dataPoints.map(item => [item.time, item.yValue]); // 计最小和最大时间戳 const minTimestamp = dataPoints[0].time; const maxTimestamp = dataPoints[dataPoints.length - 1].time; // 配置echarts选项 const option = { xAxis: { type: 'value', min: minTimestamp, max: maxTimestamp, splitNumber: dataPoints.length - 1, // 建议分成这么多段,从而有这么多刻度 axisLabel: { formatter: function (value) { // 在dataPoints中查找与value最接近的时间戳(允许误差一天内) const point = dataPoints.find(item => Math.abs(item.time - value) < 86400000 / 2); if (point) { return point.label; } return ''; // 不显示 } }, splitLine: { show: false // 不显示分割线 } }, yAxis: { type: 'value' }, series: [{ type: 'line', data: seriesData, // 可以设置一些样式 symbol: 'circle', symbolSize: 8, smooth: true // 是否平滑 }], tooltip: { trigger: 'item', formatter: function (params) { // 在tooltip中显示日和值 // params.data是一个数组,[时间戳, 数值] // 我们想要显示标签和数值 // 但是tooltip中我们不知道标签,所以我们可以通过params.dataIndex来获取标签 // 因为dataPoints和seriesData是一一对应的 const index = params.dataIndex; const label = dataPoints[index].label; const value = params.data[1]; return `${label}: ${value}`; } } }; return <ReactECharts option={option} />; }; 注意:上面的splitNumber设置为dataPoints.length-1,这样就会生成dataPoints.length个刻度(因为splitNumber是分割段数,刻度数=段数+1)。然后我们在每个刻度位置,如果该刻度值接近某个数据点的时间戳,就显示该数据点的标签。这样,每个数据点位置都会显示标签。 但是,由于计误差,有可能某个刻度没有匹配到数据点,那么就会显示为空。这样坐标轴上的标签就是我们指定的标签,且位置非均匀。 另外,在tooltip中,我们通过dataIndex来获取对应的标签,这样在鼠标悬停时显示标签和值。 但是,这里有一个问题:计时,由于每个月的天数不同,比如从1月31日加1个月,会变成228日(非闰年),然后加2个月会变成3月28日,而不是3月31日。这是符合预的吗?需求没有明确,但一般来说,加1个月就是下个月的同一日(如果下个月没有这一天,则取最后一天)。所以使用setMonth是符合JavaScript的日处理规则的。 另外,时区问题:这里使用的是本地时间,转换为时间戳(UTC时间)可能会因为时区导致显示上的差异?但时间戳是UTC的,所以全球一致。在图表上,我们显示的是标签,而不是日,所以没有问题。 最后,我们返回ReactECharts组件。 但是,我们也可以使用时间轴(type: 'time')来尝试,但是时间轴会自动格式化日,而我们希望显示的是自定义标签。所以使用value轴更直接。 我们按照上面的代码实现。 注意:如果splitNumber没有精确生成每个数据点位置的刻度,我们可以通过设置axisTick和axisLabel的interval为0,并设置splitNumber为一个较大的值,然后通过formatter控制只显示数据点位置的标签。但是这样可能会显示很多空白标签。我们上面的方法应该可以。 另外,我们也可以手动设置axisLabel的showMinLabel和showMaxLabel为true,确保首尾标签显示。但我们的formatter已经保证了首尾标签会显示(因为首尾时间戳在min和max处,并且splitNumber设置后,首尾刻度一定存在)。 因此,我们采用上述方案。 完整代码:</think>以下是在React中使用ECharts实现非均匀间隔折线图的完整解决方案。核心思路是将时间标签转换为实际日时间戳,通过时间轴类型实现非均匀间隔,并自定义标签显示。 ```jsx import React, { useRef, useEffect } from 'react'; import * as echarts from 'echarts'; const TimeSeriesChart = () => { const chartRef = useRef(null); useEffect(() => { if (!chartRef.current) return; // 基准日(当前日) const baseDate = new Date(); // 标签与日规则 const labelConfig = [ { label: 'sw', offset: { weeks: 1 } }, { label: '2w', offset: { weeks: 2 } }, { label: '3w', offset: { weeks: 3 } }, { label: '1M', offset: { months: 1 } }, { label: '2M', offset: { months: 2 } }, { label: '3M', offset: { months: 3 } }, { label: '4M', offset: { months: 4 } }, { label: '5M', offset: { months: 5 } }, { label: '6M', offset: { months: 6 } }, { label: '9M', offset: { months: 9 } }, { label: '1Y', offset: { years: 1 } } ]; // Y轴数据 const yData = [2, 7, 3, 5, 10, 29, 3, 5, 6, 9, 10]; // 生成坐标点数据 const dataPoints = labelConfig.map((config, idx) => { const date = new Date(baseDate); if (config.offset.weeks) { date.setDate(date.getDate() + config.offset.weeks * 7); } if (config.offset.months) { date.setMonth(date.getMonth() + config.offset.months); } if (config.offset.years) { date.setFullYear(date.getFullYear() + config.offset.years); } return { time: date.getTime(), // 转换为时间戳 label: config.label, value: yData[idx] }; }); // 提取时间戳和值 const seriesData = dataPoints.map(p => [p.time, p.value]); // 创建标签映射表(用于坐标轴格式化) const labelMap = {}; dataPoints.forEach(p => { labelMap[p.time] = p.label; }); // ECharts配置 const option = { xAxis: { type: 'time', axisLabel: { formatter: function(value) { // 查找最接近的时间戳对应的标签 const timestamps = Object.keys(labelMap).map(Number); const closest = timestamps.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev ); // 如果误差在可接受范围内(1天),返回标签 return Math.abs(closest - value) < 86400000 ? labelMap[closest] : ''; } }, axisLine: { onZero: false } }, yAxis: { type: 'value' }, series: [{ type: 'line', data: seriesData, symbol: 'circle', symbolSize: 10, lineStyle: { width: 3 }, emphasis: { focus: 'series' } }], tooltip: { trigger: 'item', formatter: function(params) { const date = new Date(params.value[0]); return `${labelMap[params.value[0]]}<br/> 值: ${params.value[1]}<br/> 日: ${date.toLocaleDateString()}`; } }, grid: { left: '10%', right: '10%', bottom: '15%' } }; const chart = echarts.init(chartRef.current); chart.setOption(option); return () => chart.dispose(); }, []); return <div ref={chartRef} style={{ width: '100%', height: '500px' }} />; }; export default TimeSeriesChart; ``` ### 代码解释: 1. **日逻辑**: - 使用`baseDate`作为当前日基准 - 根据标签规则计偏移量(周/月/年) - `setDate()`/`setMonth()`/`setFullYear()`处理日偏移 2. **数据处理**: - 生成`dataPoints`数组,包含时间戳、标签和值 - 构建`seriesData`为二维数组`[[timestamp, value], ...]` - 创建`labelMap`映射时间戳到标签(用于坐标轴格式化) 3. **ECharts关键配置**: - **xAxis.type='time'**:使用时间轴实现非均匀间隔 - **axisLabel.formatter**:自定义标签显示逻辑 - 查找最接近的时间戳 - 允许±1天的误差容限 - **tooltip.formatter**:显示原始标签和格式化日 4. **可视化优化**: - 圆形数据点标记(symbol: 'circle') - 响应式布局(grid配置) - 悬停高亮效果(emphasis配置) - 组件卸载时销毁图表实例 ### 效果说明: - X轴显示原始标签:`['sw', '2w', ..., '1Y']` - 坐标间隔根据实际日差非均匀分布 - 悬停提示框显示原始标签和格式化日 - 折线连接所有数据点,间距反映实际时间
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值