Thử thách lập trình JavaScript về 1 thứ tưởng đơn giản nhưng rối não

Chào mọi người,

Mùa Covid này (và cả trước đó) anh em làm toàn phải tham gia hỏi - đáp những cái hoặc quá ABC hoặc quá cao siêu về giải thuật hoặc các công nghệ mới rất ghê, rồi nào là AI các kiểu… có vẻ là mệt mỏi rồi.

Giờ đây, khi lọc dữ liệu để kiểm tra mình mới kinh hoàng nhận ra hoá ra là có những thứ tưởng chừng đơn giản, dễ giải quyết nhưng nếu không chú trọng ngay từ đầu lại tiềm ẩn bug có nguy cơ làm cho mớ dữ liệu trở thành đống rác. Bật mí luôn: liên quan đến việc kiểm tra ngày tháng mà nếu không thực hiện ở phía server khi làm web, chỉ dựa vào client là… toang.

Chúng ta thử cùng tham gia giải quyết vấn đề này mà không dùng đến việc tra cứu StackOverflow/ GitHub/ thư viện/ framework. Tất nhiên các bạn được phép tra cứu tài liệu về JavaScript để xem nó trang bị những gì.

Cụ thể luôn thử thách này như sau:

Dùng JavaScript viết một hàm để kiểm tra ngày tháng (nhập vào theo định dạng Việt Nam dd-mm-yyyy) có hợp lệ hay không.

Để tránh phức tạp vấn đề, chúng ta chỉ nhập ngày tháng từ 01-01-1900 cho đến hiện tại mà không có những ngày tháng quá xa xăm về quá khứ/ tương lai. Ngày tháng hợp lệ theo dương lịch Gregorian đang dùng phổ biến trên thế giới. Code phải chạy đúng cho các trình duyệt Google Chrome, Apple Safari, Mozilla Firefox.

Hy vọng tìm được các lời giải có code ngắn gọn, bao phủ được hầu hết trường hợp.

Mình có đoạn code bên dưới từng viết khá lâu để nhập dữ liệu sinh viên, mới cách đây mấy ngày nhận thấy nó không xài được, bỏ lọt lưới nhiều ngày không tồn tại trên lịch.

function myDateValid(dateStrVn) {
    let sections = dateStrVn.split('-');
    let testDateString = sections[2] + '-' + sections[1] + '-' + sections[0]);
    let myDate = Date.parse(testDateString);
    if (isNaN(myDate)) {
        return false;
    }
    return true;
}

Code của các bạn “game thủ” tham gia thử thách này được đưa tạm lên ĐÂY .

Mình test thử vài mẫu rồi ghi kết quả bên dưới mỗi bài để xem liệu có trường hợp mẫu test nào “lọt lưới” hoặc bị “giết nhầm”?

[Nảy sinh 1 vấn đề, anh em nào hứng thú tham gia giải quyết: 1 ứng dụng để có thể tổ chức các thử thách như này dễ dàng hơn. Ứng dụng đó có thể đáp ứng được yêu cầu là người tham gia đăng bài dự thi lên, có thể chạy được code để xem kết quả, có giám khảo chấm, và có phần cho khán giả tham gia bình luận]

Đọc cái này mới thấy cán bộ mà dốt thì khổ dân.

6 Likes

Challenge accepted

//Dựa trên: https://vi.wikipedia.org/wiki/Năm_nhuận
function is_leap_year(year){
	if (year % 100 != 0){
		if (year % 4 == 0) return true
		return false
	}
	if (year % 400 == 0) return true
	return false
}
function max_day(month, year){
	if (month >= 1 && month <= 7){
		if (month == 2) return is_leap_year(year) ? 29 : 28
		return month % 2 == 1 ? 31 : 30
	}
	return month % 2 == 1 ? 30 : 31
}
function check_range(day, month, year){
	if (day == 0 || day >= 32) return false
	if (month == 0 || month >= 13) return false
	if (day > max_day(month, year)) return false
	return true
}
function check_date(input) {
	const date_regex = /^([0-9]{2})-([0-9]{2})-([0-9]{4})$/
    const date_match = input.match(date_regex)
	if (!date_match){
		console.log("Input's format is not vaild")
		return false
	}
    const day = parseInt(date_match[1],10)
    const month = parseInt(date_match[2],10)
    const year = parseInt(date_match[3],10)
    if (!check_range(day, month, year)){
		console.log("Day, month or year out of range")
		return false
	}
	return true
}
check_date("31-02-2000")

P/s: Chưa tét :penguin:
P/s 2: Test lại mới biết code chạy hơi… :sweat_smile:. Chắc lần này được rồi

4 Likes
function myDateValid(dateStrVn) {
    const date_regex = /^([0-9]{2})-([0-9]{2})-([0-9]{4})$/;
    const date_match = input.match(date_regex)
	if (!date_match){
		console.log("Input's format is not vaild")
		return false
	}
    let [d,m,y] = dateStrVn.split('-');
    let myDate = new Date(+y, +m - 1, +d);
    
    return +d == myDate.getDay() && +m == myDate.getMonth() + 1 && +y ==  myDate.getYear() ;
}
7 Likes

Như đã biết:

  • Không có năm 0.

  • Tháng 1, 3, 5, 7, 8, 10, 12 có 31 ngày.

  • Tháng 4, 6, 9, 11 có 30 ngày.

  • Tháng 2 có 28 ngày với năm không nhuận và 29 ngày với năm nhuận.

  • 1 số chia hết cho 4 khi số tạo bởi chữ số hàng chục và chữ số hàng đơn vị của nó là 1 số chia hết cho 4.

  • Năm nhuận là năm chia hết cho 400 hoặc không chia hết cho 100 mà chia hết cho 4. Tức là, nếu biểu diễn năm là 1 số có 4 chữ số \overline{abcd}:

if\ \overline{cd} = 0 \Rightarrow 4\ |\ \overline{ab}\\ else\ 4\ |\ \overline{cd}
  • Các số có 2 chữ số sau đây chia hết cho 4:
    • 00, 04, 08, 20, 24, 28, 40, 44, 48, 60, 64, 68, 80, 84, 88
    • 12, 16, 32, 36, 52, 56, 72, 76, 92, 96

Ta đã xong bước đầu tiên. Giờ ta sẽ liệt kê các pattern thoả mãn 1 ngày tháng cụ thể.

Ver 1.x - Cách tiếp cận 1:

  • Tháng != 2:

    • 30 ngày đầu tiên: /([0-2]\d|30)-(0[13-9]|1[0-2])-(?!.*0000)\d{4}/.
    • Ngày thứ 31: /31-(0[13578]|1[02])-(?!.*0000)\d{4}/.
  • Tháng 2:

    • 28 ngày đầu tiên: /([01]\d|2[0-8])-02-(?!.*0000)\d{4}/.
    • Ngày thứ 29: 29-02-????. Câu hỏi đặt ra là, ta sẽ điền cái gì để thế vào ???? đây?

Quay lại với các số có 2 chữ số mà chia hết cho 4. Ta xét riêng trường hợp 00 tận cùng:

  • Năm không chia hết cho 100 mà chia hết cho 4: /\d\d([13579][26]|[2468][048]|0[48])/
  • Năm chia hết cho 400: /([13579][26]|[2468][048]|0[48])00/ (lưu ý không có năm 0).

Nếu ta ghép 2 pattern năm trên thành 1 pattern thật ngắn gọn thì sao nhỉ?

Ta có thể dùng 1 regex /(\d\d)?([13579][26]|[2468][048]|0[48])(00)?/. Tuy nhiên, pattern này có thể nhận vào 2, 4 hoặc 6 ký tự. Ta có thể kiểm tra độ dài của cả string ngày tháng trước đó, nhưng đích đến của ta là chỉ dùng regex để verify ngày tháng năm hợp lệ. Chặng đường vẫn còn dài…


Trước hết, ta tổng kết qua 1 lượt để ra được đoạn code ver 1.0:

function myDateValid(dateStrVn) {
  const validPattern = [
    /^(?:[0-2]\d|30)-(?:0[13-9]|1[0-2])-(?!.*0000)\d{4}$/,
    /^31-(?:0[13578]|1[02])-(?!.*0000)\d{4}$/,
    /^(?:[01]\d|2[0-8])-02-(?!.*0000)\d{4}$/,
    /^29-02-(\d\d(?:[13579][26]|[2468][048]|0[48])|(?:[13579][26]|[2468][048]|0[48])00)$/
  ]
  // ES6
  return validPattern.some(p => p.exec(dateStrVn));
}

Code ver 1.1:

function myDateValid(dateStrVn) {
  const validPattern_v11 = [
    /^(?:[0-2]\d|30)-(?:0[13-9]|1[0-2])-(?!.*0000)\d{4}$/,
    /^31-(?:0[13578]|1[02])-(?!.*0000)\d{4}$/,
    /^(?:[01]\d|2[0-8])-02-(?!.*0000)\d{4}$/,
    /^29-02-(?:(\d\d)?(?:[13579][26]|[2468][048]|0[48])(00)?)$/
  ]
  // ES6
  return dateStrVn.length == 2+1+2+1+4 && validPattern_v11.some(p => p.exec(dateStrVn));
}

Ver 2.x - Cách tiếp cận 2:

  • 28 ngày đầu tiên: /([01]\d|2[0-8])-(0\d|1[0-2])-(?!.*0000)\d{4}/.

  • Ngày thứ 29, 30 cho tháng != 2: /(29|30)-(0[13-9]|1[0-2])-(?!.*0000)\d{4}/.

  • Ngày thứ 31: /31-(0[13578]|1[02])-(?!.*0000)\d{4}/.

  • Ngày 29/02 năm nhuận: tương tự ver 1.

Code ver 2:

function myDateValid(dateStrVn) {
  const validPattern_v2 = [
    /^(?:[01]\d|2[0-8])-(?:0\d|1[0-2])-(?!.*0000)\d{4}$/,
    /^(?:29|30)-(0[13-9]|1[0-2])-(?!.*0000)\d{4}$/,
    /^31-(?:0[13578]|1[02])-(?!.*0000)\d{4}$/,
    /^29-02-(\d\d(?:[13579][26]|[2468][048]|0[48])|(?:[13579][26]|[2468][048]|0[48])00)$/
  ]
  // ES6
  return validPattern_v2.some(p => p.exec(dateStrVn));
}

Rút gọn pattern 29/02, ta được code ver 2.1 tương tự.


Điểm cộng

  • Chỉ sử dụng regex (trừ ver x.1 có thêm check điều kiện độ dài).

Điểm trừ

  • Chưa phủ được trường hợp xx-xx-0000 với xx-xx != 29-02. --> Đã cải thiện bằng cách thay thế /\d{4}/ bằng /(?!.*0000)\d{4}/.

Ver 3 - Tà đạo :smiling_imp:

Hôm nay mình đua đòi chút, áp dụng một chút kỹ năng golf để chọc tức các dev khác chơi :smiling_imp:

Bấm vào nếu bạn đã sẵn sàng

Version hơi bình thường:

function myDateValid(ddmmyyyy_date) {
  yyyymmdd_date = ddmmyyyy_date.split('-').reverse().join('-');
  try {
    yyyymmdd_full = new Date(yyyymmdd_date).toISOString();
  } catch (err) {
    return false;
  }
  return yyyymmdd_full.startsWith(yyyymmdd_date+"T");
}

Version tà đạo:

function myDateValid(d) {
  try { return new Date(d = d.split`-`.reverse().join`-`).toISOString().startsWith(d+"T") } catch (err) { return false }
}
7 Likes

Đã bổ sung thêm phiên bản đã sửa, thử vào test xem: https://js-challenge.khoancatbetong.com/01.html

Không edit https://js-challenge.khoancatbetong.com/ để chạy lại dc à bạn.

3 Likes

Đang là web tĩnh 100%, mình soạn bằng gEdit từng file một :frowning:. Hiện vẫn chưa biết làm sao để gắn cái Codepen hoặc jsFiddle hoặc tương đương vì năng lực về lập trình web còn thấp. Cho nên, bạn cứ làm bên nào đó rồi gửi link, mình sẽ cập nhật vào và tìm giải pháp nào đó tốt hơn.

Cũng có thể anh em trong này hôm nào tụ tập làm một cái Project tập thể nào đó chơi chơi phục vụ chính anh em, biết đâu sau này nó thành cái gì đó như Spiderum :smiley:

5 Likes

pushlish lên github dc không,
& nhờ cập nhật lại solution bên trên.
:white_heart: :white_heart: :white_heart:

2 Likes

Mình vẫn chưa biết dùng GitHub ngoài lệnh clone duy nhất. Bạn có thể đẩy code lên đó và cho link để mình lấy về trang kia, đang loay hoay làm sao để nó giống như một đấu trường kiểu người ta đánh cờ ấy, tuy không trực tiếp như chơi cờ nhưng khán giả có thể vào xem để thấy các game thủ đang chơi thế nào. Người có kiến thức chuyên môn có thể bình luận code đó là đang hay/ dở/ tối ưu/ dài dòng/ buồn cười,… thì thú vị.

3 Likes
  1. Đầu tiên bạn tạo 1 git repo trên github
  2. mở git repo mới tạo
  3. copy đường link clone về
  4. Mở terminal trong thư mục source code
git init (nếu chưa init)
git remote add origin [email protected]:User/UserRepo.git

[email protected]:User/UserRepo.git là đường link copy từ bước 3

Sau đó gửi đường link lên cho mọi người.
Các bước tiếp theo, tạo PR, merge code, tạo branch chỉ tương tự với git thôi.

Mong chờ quá.

4 Likes

Đọc nghe có vẻ đơn giản nhưng làm theo thì bắt đầu thấy git là một cái khá rối rắm chứ không đơn giản tí nào, đúng là Mr Linus trùm viết code C nên tư duy của him cũng không dễ để hiểu thế nào.

Giờ các bạn thử vào https://github.com/superthin/js-challenge/tree/shared và pull rồi commit thử xem có được không nhé.

Hiện nay mình vẫn chưa biết cách làm sao để cho phép các bạn tham gia commit lên. Với suy nghĩ trong đầu là tạo một thư mục shared như trên hệ điều hành và cấp quyền đọc, ghi cho nó. Nhưng dường như đó không phải là cách git làm việc, cũng chưa tìm thấy chỗ nào trên GitHub cho phép điều này hoặc cách tiếp cận sao đó, đọc đoạn này nhưng vẫn chưa hiểu.

Qua cái topic này chúng ta cùng học hỏi, có một vài trò “bá đạo” thật thú vị, đang cảm thấy có chút động lực :smiley:

5 Likes
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?