JavaScript 演算法:計算最大連續日期區間
上個星期,幫我的一個前端好友搞定了一個演算法,本人對 JavaScript 熟悉程度一般,但是瞭解 JS 的面相物件的一些東西,經過少量查閱,最終弄完了這個演算法,或許這個演算法,在 C# 或者 Java 看來並不是很難,而且由於高階語言自身的框架類庫非常的豐富,各種方法操作非常的便捷,尤其是語法糖豐富多彩的 C#,寫這些演算法,不考慮可讀性的話,應該可以以更少的程式碼來實現。
這小東西花了一定的時間,所以特來分享。
首先來說一下,需求的描述:該公司網站需要在下拉顯示的日曆上,底部位置顯示出使用者多選的日期,如果連續,則顯示日期範圍,比如 2015.1.1 ~ 2015.1.5;如果是單個日期,就直接顯示日期。
好了,問題來了,需求挺簡單的,但是在接下來的實現中遇到各種不大不小的坑和結節,比如:
與閏年相關的二月份的總天數
大月(總天數 31 天)和小月(總天數 30 天)的區別對待,以及對於 12 月的特殊對待(稍後會說到)
給定的條件是一組日期字串,而不是日期物件
如何判定兩個字串日期是否連續?(在這個判定的演算法上,會引發另外一些問題,其中就包括對於 12 月的特殊對待)
接下來,是我對於這個問題的分析和解決方法的步驟(如有更好的建議,歡迎詳細提出):
首先,我想到的是,我們應該在日期物件上做文章,而不是單純的留在字串上,對字串進行一番折騰,這是很費時間和功夫的,而且吃力不討好。所以,我決定將字串通過一個 function 轉成 Date 物件,後續的操作可以帶來便捷。
所以,當時第一反應,很簡單嘛,比如像下面這樣:
var date = new Date("2015.1.1")
由於該公司網站需要支援 IE 11 以下的版本,以及 Firefox,這個看似沒有問題的寫法,其實是有相容性問題的,這個語法在 IE 11 以下版本以及火狐上是沒法執行的(PS:嘿嘿,在幫助別人的同時,實際也在碎片化的積累自己的知識,不是嗎?)
所以,經過我朋友的修改,這個方法最終變成下面這個樣子:
String.prototype.ToDate = function () { var parts = this.split('.'); var date = new Date(); date.setUTCFullYear(parts[0], parts[1] - 1, parts[2]); date.setUTCHours(0, 0, 0, 0); return date; }
我把它融入到 prototype 裡面去,這樣程式碼看起來會比較優雅和美觀,JS 裡面的這個語法,其實和 C# 的擴充套件方法的效果是一樣的。
接下來,就是排序,因為給定的日期字串是亂序的,所以,排序完之後,再對其進行演算法解析。
顯然預設的排序規則不適合,我們需要自定義排序規則,對於兩個日期物件,我們只要比較年月日即可,如果不相等,則直接讓他們相減即可知道大小。所以,這個外部方法可以向下面這樣寫,有了它,陣列可以方便的排序了,這是第一步。
function compare(md1, md2) { md1 = md1.ToDate(); md2 = md2.ToDate(); if (md1.toLocaleDateString() == md2.toLocaleDateString()) { return 0; } return md1 - md2; }
然後,下面的一個關鍵演算法是,如何得知相鄰的日期是否連續,這是演算法迴圈中判斷邏輯的核心,思路就是獲取前一個日期(因為經過排序,肯定小於等於後一個)的下一個日期,然後將其和第二個日期進行判等比較。
判等不難,逐個比較年、月、日,但有一點需要注意,判斷年份相等不是 getYear 而是 getFullYear。(滿坑爹的)
Date.prototype.equals = function (md) { if (md == null) return false; return this.getFullYear() == md.getFullYear() && this.getMonth() == md.getMonth() && this.getDate() == md.getDate(); }
對於獲取下一個日期的演算法,我們需要區別:是否是閏年、是大月還是小月。所以在此地,我們應該先對於月份進行判斷:
Date.BIG_MONTH = 0; // 31 天大月 Date.SML_MONTH = 1; // 30 天小月 Date.FEB_MONTH = 2; // 二月 Date.INVALID_MONTH = -1; // 月份不合法 Date.prototype.getMonthType = function () { var month = this.getMonth() + 1; // 根據總天數,對月份分組 var months31 = "1, 3, 5, 7, 8, 10, 12"; var months30 = "4, 6, 9, 11"; if (months31.indexOf(month.toString()) >= 0) { return Date.BIG_MONTH; } else if (months30.indexOf(month.toString()) >= 0) { return Date.SML_MONTH; } else if (month == 2) { return Date.FEB_MONTH; } else { return Date.INVALID_MONTH; } }
getMonthType 不難看懂,返回三種合法型別的月份型別,外加一個 Invalid 返回值。
接著判斷是否是閏年,因為這個決定了二月份的總天數,總天數決定了下一天是幾月幾號。
Date.prototype.isLeapYear = function () { var year = this.getFullYear(); return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; }
這個判斷閏年的演算法,就不多說了,大學課程裡 C 語言程式設計必會的基本演算法之一。
有了上面幾個函式做鋪墊,獲取下一天的日期可以這麼寫:
Date.prototype.getNextDate = function () { var year = this.getFullYear(); // 獲取月份 var month = this.getMonth() + 1; // 獲取日期 var day = this.getDate(); switch (this.getMonthType()) { // 如果是大月 case Date.BIG_MONTH: if (day == 31) { day = 1; if (month == 12) { month = 1; year += 1; } else month += 1; } else day += 1; break; // 如果是小月 case Date.SML_MONTH: if (day == 30) { day = 1; month += 1; } else day += 1; break; // 如果是二月 case Date.FEB_MONTH: var totalDays = this.isLeapYear() ? 29 : 28; if (day == totalDays) { day = 1; month += 1; } else day += 1; break; // 不合法月份 default: return null; } return (year + "." + month + "." + day).ToDate(); }
對於 12 月,需要特殊對待,因為 12 月 31 日的下一天是 1 月 1 日,月份和日期均不能直接遞增,所以處理 12 月份的邏輯需要分開。最後一句返回一個日起物件,這麼寫是因為,new Date(year, month, day) 同樣在 IE 低版本的瀏覽器是不相容的。
接著判斷日期 B 是不是 日期 A 的下一個日期,就比較方便了:
Date.prototype.isNext = function (md) { var next = this.getNextDate(); return next.equals(md); }
最後,兩個演算法,一個是用來獲取一組日期中最長連續日期的範圍,另一個是將所有日期,如果連續就輸出範圍,不連續就單個輸出。
function getLongestContinualDateRange(dates) { if (dates.length == 0) return []; // 如果陣列只有一個元素,直接返回陣列長度,無須比較 if (dates.length == 1) return [new Range(0, 1)]; // 先排序,使之成為有序陣列 dates.sort(compare); // 最大連續日期範圍長度(還沒有開始比較,預設第一個日期,長度 1) var longest_length = 1; // 最大連續日期範圍的起始索引(還沒有開始比較,預設為 0) var longest_length_begin_index = 0; // 並列最大長度範圍的集合 var ranges = [new Range(longest_length_begin_index, longest_length)]; for (var current = 0, next = 1; next < dates.length; next++) { // 將當前索引的字串轉成日期物件 var current_date = (current == next) ? dates[current].ToDate() : dates[next - 1].ToDate(); var next_date = dates[next].ToDate(); // 判斷 next_date 是否是 current_date 的明天 if (!current_date.isNext(next_date)) { // 重置下一次可能的長範圍的開頭位置 current = next; } // 得到新的連續日期範圍的長度 var new_length = next - current + 1; // 產生新的最大長度,之前的統統作廢 if (longest_length < new_length) { longest_length = new_length; // 最長範圍改變的時候,可能會改變起始索引,因為可能是另外一段 if (longest_length_begin_index != current) { longest_length_begin_index = current; } // 清空最長範圍集合,之前的最長範圍統統作廢 ranges.length = 0; ranges.unshift(new Range(longest_length_begin_index, longest_length)); } // 產生新的與當前最大長度等長的日期範圍,存入 ranges 集合 else if (longest_length == new_length) { ranges.unshift(new Range(current, new_length)); } } return ranges; }
演算法描述:首先對陣列進行長度判斷,如果沒有元素則直接返回空陣列,不返回 null 是為了更好的相容錯誤,如果長度等於 1,則直接返回一個長度為 1 的範圍(自定義的 Range 物件,稍後提到)。接著,假定陣列是無序的,對陣列進行從小到大的排序。定義兩個變數,分別表示最大長度和長度的起始索引,預設就是第一個元素,長度為 1。由於當存在同樣大小的範圍時,需要全部保留,將所有的 Range 加入一個集合。此處定義一個最大長度範圍的集合 ranges。
迴圈初始的時候,current 指標指向第一個元素,next 指向它的下一個。接下來,迴圈開始轉動,current_date 被設為 next 指標指向的日期的前一個日期,但是當被重置的時候,current 指標和 next 指標指向的是同一個日期,所以此時應該取 current 的值,來判定兩者是否是連續關係。
如果 current date 的後一天不是 next date,那麼 current 設為 next 指向的日期,以重置 range 的新開頭,說明此時 range 已經斷了,不是連續的了。
接下來計算 current 和 next 之間的跨度,如果等跨度產生,則新增至集合 ranges,如果產生新的跨度,則表明之前的 max range 統統作廢,新的日期範圍已經產生!直至跳出 for 迴圈,返回收集的最大跨度集合。
取所有的日期跨度的演算法這個相對簡單一點,演算法略有不同。程式碼如下:
function mergeToDateRange(dates) { if (dates.length == 0) return []; // 如果陣列只有一個元素,直接返回陣列長度,無須比較 if (dates.length == 1) return [new Range(0, 1)]; // 先排序,使之成為有序陣列 dates.sort(compare); // 並列最大長度範圍的集合 var ranges = new Array(); for (var begin = 0, p = 0; p < dates.length; ) { var date_p = dates[p].ToDate(); var date_pn = (p < dates.length - 1) ? dates[p + 1].ToDate() : null; if (!date_p.isNext(date_pn)) { ranges.push(new Range(begin, p - begin + 1)); begin = ++p; } else { p++; } } return ranges; }
核心就是,當遇到不連續的點的時候,馬上將之前的連續的範圍儲存下來,然後從新位置繼續執行計算。
Range 類用來表示一個陣列中的連續範圍,isSingle 表示是否是單個日期,而非連續範圍。
function Range(index, length) { this.index = index; this.length = length; this.isSingle = length == 1; }
一個演算法的實現中,演算法思想就好比一朵花;作為綠葉,為實現演算法所做的各種‘填坑’操作以及輔助方法,也是很重要的。
測試程式碼可以這樣:
var dateArray = ['2015.7.11', '2015.7.12', '2015.7.14', '2016.2.1', ......];
dateArray.sort(compare);
// 計算最大範圍日期
var ranges = getLongestContinualDateRange(dateArray);
document.write("最大連續日期:</br>");
printRanges(ranges);
// 計算所有範圍日期
ranges = mergeToDateRange(dateArray);
document.write("所有日期範圍:</br>");
printRanges(ranges);
function printRanges(ranges)
{
for (var i = 0; i < ranges.length; i++)
{
if (ranges[i].isSingle)
{
document.write(" " + dateArray[ranges[i].index]);
}
else
{
document.write(" " + dateArray[ranges[i].index] +
" ~ " + dateArray[ranges[i].index + ranges[i].length - 1]);
}
document.write("</br>");
}
}
相關文章
- Hive計算最大連續登陸天數Hive
- javascript計算指定日期增加多長時間後的日期JavaScript
- javascript如何計算兩個日期之間的時間間隔JavaScript
- javascript 計算兩個日期間差的天數JavaScript
- javascript計算兩個時間日期相差的天數JavaScript
- 計算2個日期間的所有日期
- JS-計算日期差值;計算日期之間的月數JS
- excel日期加減計算方法 excel計算日期時間差Excel
- javascript計算兩個日期之間的時間差程式碼例項JavaScript
- 10個SQL技巧之四:找到連續的沒有間隙的最大系列的日期SQL
- php日期時間計算,轉載PHP
- JavaScript計算兩個日期相差天數JavaScript
- JavaScript 時間日期操作JavaScript
- javascript計算兩個日期相差的天數JavaScript
- 日期計算
- mysql查詢中時間、日期加減計算MySql
- 演算法筆記_043:最大連續子陣列和(Java)演算法筆記陣列Java
- 日期區間查詢
- JavaScript 動態時間日期JavaScript
- JavaScript動態時間日期JavaScript
- JavaScript根據出生日期計算年齡JavaScript
- Java中計算兩個日期間的天數Java
- ORACLE 計算2個日期之間的天數Oracle
- 計算2個日期間有多少個自然周
- 25:計算兩個日期之間的天數
- Oracle計算兩個日期之間的天數Oracle
- 最大連續子陣列和(最大子段和)陣列
- sql關於連續日期的統計報表問題SQL
- 【演算法題解】485. 最大連續1的個數 - Java演算法Java
- 日期計算器
- SQL 10 函式 3 日期時間函式 - 5 計算日期差額SQL函式
- JavaScript計算時間差詳解JavaScript
- 連續子陣列的最大和陣列
- JavaScript時間日期格式化JavaScript
- JavaScript 時間日期格式轉換JavaScript
- JavaScript比較時間日期大小JavaScript
- 演算法學習-零子陣列,最大連續子陣列演算法陣列
- JavaScript獲取兩個日期之間所有的日期JavaScript