Bài tập VBA cho người mới bắt đầu

Liên hệ QC

SA_DQ

/(hông là gì!
Thành viên danh dự
Tham gia
8/6/06
Bài viết
14,320
Được thích
22,361
Nghề nghiệp
Nuôi ba ba & trùn quế
Các bạn viết cho 1 macro để ta có được kết quả như bảng sau:

2/29/2004​
2/29/2016​
2/29/2000​
2/29/2012​
2/29/2024​
2/29/2008​
2/29/2020​
2/29/2032​
2/29/2044​
2/29/2028​
2/29/2040​
2/29/2052​
2/29/2036​
2/29/2048​
2/29/2060​
2/29/2072​
2/29/2056​
2/29/2068​
2/29/2080​
2/29/2064​
2/29/2076​
2/29/2088​
2/29/2084​
2/29/2096​
2/29/2092​
 
Bài tập 2:
4 test ví dụ bài #26 không có cái nào chứa năm '000 :unknw:
Test như vậy là chưa đủ :p
...
' bỏ qua năm '00, nhưng giữ lại năm '000
1. Bài 26 là 4 ví dụ minh họa chứ không phải test 4 lần. Các ví dụ cho thấy các tình huống yêu cầu:
- chọn năm bắt đầu và kết thúc bất kỳ
- Khoảng thời gian bất kỳ (nếu thêm 1 ví dụ từ năm sinh đến hiện tại ra 64 năm thì tốt)
- Vượt sang thế kỷ sau và vượt cả thiên niên kỷ

Năm x000 có phải nhuận hay không là người giải phải tự làm, chứ tôi không cho ví dụ, và tôi không cần test riêng trường hợp đó vì hàm VBA sẽ loại bỏ hoặc tiếp nhận mà không cần quan tâm.

2. Theo tôi nhớ thì không phải tròn ngàn sẽ nhuận, mà là tròn 400 năm sẽ nhuận: Năm chia hết cho 400 sẽ nhuận, năm 3000, 5000, 7000 là tròn ngàn nhưng không chia hết cho 400 nên không nhuận. Năm 4000, 6000, 8000 chia hết cho 400 nên nhuận
Có test đàng hoàng bằng VBA và cả hàm Date của worksheet. Ô tô đỏ là tròn ngàn nhưng không nhuận, Ô tô màu xanh là ô chia hết cho 400
Ô C2 chữ đỏ vì VBA đúng, hàm Date sai cho năm 1900

1707272029446.png
 
Lần chỉnh sửa cuối:
Upvote 0
Upvote 0
Bài tập 2
Đề bài rõ ràng hơn bài 1 :p :p :p
Thực hiện công việc sau:
Cho ô C1 chứa năm bắt đầu và F1 chứa năm kết thúc mong muốn. Liệt kê những ngày 29/02 của những năm nhuận trong khoảng thời gian bắt đầu/ kết thúc trên vào bảng theo thứ trong tuần, đầu tuần là thứ hai ( viết tắt Mon ở A2) như hình bên dưới:
Kết quả phải là ngày dạng chuẩn và tự động canh phải.
Cho phép chọn năm bắt đầu và năm kết thúc bất kỳ (không lệ thuộc vào đầu cuối thế kỷ, không giới hạn 100 năm)
Với phân tích giải thuật ở bài #10, sẽ thấy số dòng không tự động tăng trong 1 số trường hợp, và 1 dòng không phải lúc nào cũng điền đầy rồi mới qua dòng kế.

Minh họa: Chọn từ 2001 đến 2100 (100 năm)

View attachment 298963

Cũng 100 năm, bắt đầu 2512


View attachment 298967

Chọn từ 2005 đến 2154 (150 năm)

View attachment 298965

Chọn từ 3340 đến 3539 (200 năm)

View attachment 298966
Thử code
Mã:
Sub XYZ()
  Dim res(), a&(1 To 7), fYear&, lYear&, i&, j&, t
 
  fYear = Range("C1").Value 'Nam dau
  lYear = Range("F1").Value 'Nam cuoi
  ReDim res(1 To (lYear - fYear + 1) \ 7 + 5, 1 To 7)
 
  For i = fYear To lYear Step 4
    t = i & "/2/29"
    If IsDate(t) Then
      j = Weekday(t, 2)
      a(j) = a(j) + 1
      res(a(j), j) = t
    Else
      i = i - 3
    End If
  Next i
  Range("A3").Resize(UBound(res), 7).Value = res
End Sub
 
Upvote 0
Thử code
Mã:
  ReDim res(1 To (lYear - fYear + 1) \ 7 + 5, 1 To 7)
End Sub
Redim số dòng hơi dư, chia 28 thì hợp lý hơn. Cộng 5 là phòng xa thì tốt rồi.
Dùng biến mảng 1 chiều 7 phần tử để lưu trữ số phần tử của từng cột là biện pháp hay nhất, tôi cũng dùng như vậy.
 
Upvote 0
2. Theo tôi nhớ thì không phải tròn ngàn sẽ nhuận, mà là tròn 400 năm sẽ nhuận: Năm chia hết cho 400 sẽ nhuận, năm 3000, 5000, 7000 là tròn ngàn nhưng không chia hết cho 400 nên không nhuận. Năm 4000, 6000, 8000 chia hết cho 400 nên nhuận
...
Xin lỗi, bi giờ tôi nhớ lại rồi. Đúng con toán ngoại lệ là 100 nhưng không phải 400. Bài toán tính năm nhuận là:
And(Mod(nam, 4)=0, Or(Mod(nam, 400)=0, Mod(nam, 100) <> 0))

Code trên phải sửa
If ((yr Mod 100) <> 0) Or ((yr Mod 1000) = 0) Then ' bỏ qua năm '00, nhưng giữ lại năm '000
sửa lại
If ((yr Mod 400)=0) Or ((yr Mod 100)<>0) Then ' bỏ 100, nhưng giữ lại 400

Chú:
Quote từ The Science of Leap Year, Bob Craddock, 27/02/2020
1707286324142.png
 
Upvote 0
Đáp án cho câu 2, không phải là tối ưu nhưng đơn giản:
- 1 vòng lặp
- Dùng DateSerial để khỏi bị lỗi, và kết quả luôn luôn là ngày tháng, kể cả DateSerial(2450, 13, 32) (Ngày 32 tháng 13)
- KIểm tra năm không phải năm nhuận bằng hàm Day <> 29 (hàm Month <>2 cũng được)
- Dùng hàm Ceiling để lấy năm chia hết cho 4 đầu tiên, không cần lấy năm nhuận cuối cùng
- Dùng mảng tạm để lưu trữ thứ tự dòng từng cột, mỗi cột thì số lượng dòng tăng riêng)
- Khai báo mảng kết quả với số dòng dư cho 1000 năm, nhưng dùng biến MaxRw để chỉ đưa xuống sheet số dòng có dữ liệu. Tính trước như bài 43 cũng hay nhưng vẫn phải cộng 1 số dòng dư phòng xa.

Không cần ghi chú người học căn bản cũng hiểu.

PHP:
Sub Nhuan()
    Dim ColArr(1 To 7), ResultArr(1 To 45, 1 To 7), i As Long, j As Long
    Dim dayN As Date, MaxRw As Long
    Sheet2.Range("A3:G50").ClearContents
    For i = Application.Ceiling(Sheet2.[C1], 4) To Sheet2.[F1] Step 4
        dayN = DateSerial(i, 2, 29)
        If day(dayN) = 29 Then '' Loai bo nam khong nhuan
            j = Weekday(dayN, vbMonday)
            ColArr(j) = ColArr(j) + 1
            If ColArr(j) > MaxRw Then MaxRw = ColArr(j)
            ResultArr(ColArr(j), j) = dayN
        End If
    Next
    Sheet2.[A3].Resize(MaxRw, 7).Value = ResultArr
End Sub
 
Lần chỉnh sửa cuối:
Upvote 0
Nếu không nhằm, đó là tam giác Pascal;
2ui tắc là Số ở hàng dưới bằng với tổng hai số ngay trên nó.
Tam giác Pascal là một trong những bài căn bản học lập trình. Học lập trình thì phải biết toán cao cấp. Điển hình là Đại số, Giải tích, và Số học.
Tuy nhiên, VBA là lập trình ứng dụng - không chỉ lập trình mà còn sử dụng các đối tượng/object của VBA và bảng tính. Vì vậy học VBA không cần phải học toán cao cấp. Chỉ cần hiểu rõ thế nào là đối tượng, thuộc tính và phương thức.

Trong bài trên, lập tam giác Pascal không khó. Cái khó ở đây là xếp lại cho chúng nằm thành hình tháp tức tam giác cân (lúc tính thì chúng xếp thành tam giác vuông). Thêm bài trên còn chảnh choẹ, merge cells tùm lum.

Hiện tại, cứ coi như là bài tập 3
Giải thuật:, lưu ý rằng tam giác Pascal là tam giác cân về trị. Tức các trị sẽ phản chiếu qua đường trung tuyến.
(giải thuật này không làm chuyện merge cells. Tuy nhiên, để dễ đọc, kết quả sẽ ghi 2 cells một trị giống như đề bài)
1. Lập mảng có số dòng là chiều cao tam giác, số cột là chiều cao * 2 - 1
2. Vòng lặp dòng từ 1 đến chiều cao
2.1. Xác định cột ghi đầu tiên là chiều cao - dòng + 1
2.2. Vòng lặp cột từ cột đầu tiên đến chiều cao step 2 (ghi 1 ô bỏ 1 ô)
2.2.1. Nếu cột đầu tiên thì trị là 1
2.2.2. Các cột khác, dùng con toán Pascal (tổng của 2 số trên nó)
2.2.3. Dùng phép phản chiếu để ghi các cột bên phải cột chiều cao (= tổng số cột - cột + 1)
3. Ghi mảng xuống sheet
4. Hết

Mã:
Sub PascalTriangle(Optional ByVal nHeight As Long = 1)
' code vẽ tam giác Pascal với chiều cao nHeight
' để cho tam giác đều, đẹp, dùng phép ghi một ô bỏ một ô
numCols = 2 * nHeight - 1 ' tổng số cột
ReDim a(1 To nHeight, 1 To numCols)
For rw = 1 To nHeight
  firstCol = nHeight - rw + 1 ' cột đầu tiên để ghi
  For col = firstCol To nHeight Step 2 ' ghi một ô, bỏ một ô
    If col = firstCol Then
      a(rw, col) = 1
    Else
      a(rw, col) = a(rw - 1, col - 1) + a(rw - 1, col + 1)
    End If
    a(rw, numCols - col + 1) = a(rw, col) ' ô phản chiếu
  Next col
Next rw
[a1].Resize(UBound(a, 1), UBound(a, 2)) = a
End Sub
 
Upvote 0
Đầu năm thử ra thêm BÀI TẬP 4
Đề bài: cho tháng và năm, tìm số ngày có trong tháng ấy.
Ví dụ tháng 4 có 30 ngày. Đáng lẽ chỉ tháng là đủ, nhưng kẹt tháng 2 cần dữ liệu năm.

1. Sơ cấp:
Dùng bất cứ cái gì để tính.
Ghi vào ô A1 (nếu biết thêm một vài mẹo để ghi thì cứ việc dùng)

2. Trung cấp:
2.1. Không được dùng hàm VBA hay hàm bảng tính, tức là chỉ dùng con toán thuần túy học từ lớp 5, 6 gì đó.
2.2. Không dùng lệnh IF
(có một vài cách, cách hiển nhiên nhất là dùng Select Case, nhưng cũng có lệnh rẽ nhánh điều kiện khác như On ??? Goto ???)

3. Cao cấp:
Bất cứ cách đặc biệt nào bạn nghĩ ra.
Ví dụ viết một function với JavaScript rồi gọi function ấy.
 
Upvote 0
PHP:
Function Ngay(ByVal Thg As Integer, ByVal Nam As Integer) As Integer
 Ngay = Switch(Thg = 1, 31, Thg = 3, 31, Thg = 5, 31, Thg = 7, 31, Thg = 8, 31, Thg = 10, 31, _
    Thg = 12, 31, Thg = 4, 30, Thg = 6, 30, Thg = 9, 30, Thg = 11, 30, Thg = 2, IIf(Nam / 4 = Nam \ 4, 29, 28))
End Function
:D :D :D
 
Upvote 0
Mã:
Function Ngay1(ByVal Thg As Integer, ByVal Nam As Integer) As Integer
 
      Dim Th(12) As Integer
      Dim Th2(4) As Integer

      Th(1) = 31
      Th(2) = 0
      Th(3) = 31
      Th(4) = 30
      Th(5) = 31
      Th(6) = 30
      Th(7) = 31
      Th(8) = 31
      Th(9) = 30
      Th(10) = 31
      Th(11) = 30
      Th(12) = 31
 
      Th2(0) = 29
      Th2(1) = 28
      Th2(2) = 28
      Th2(3) = 28
 
      Th(2) = Th2(((Nam / 4) - (Nam \ 4)) * 4)
 
      Ngay1 = Th(Thg)
 
End Function

copy công thức từ #50
kế thừa và phát huy ^^

:D :D :D:D :D :D

phân tích toán số từ #50
thị trường ngách ^^
1707709461948.png
 
Lần chỉnh sửa cuối:
Upvote 0
Không dùng hàm VBA, không dùng If và case .
t: Tháng
n: Năm
Mã:
Function ABC(t&, n&)
  ABC = 31 + (t = 4) + (t = 6) + (t = 9) + (t = 11)
  ABC = ABC + (t = 2) * (3 + (n = (n \ 4) * 4) + (n = (n \ 100) * 100) * (n <> (n \ 400) * 400))
End Function
 
Lần chỉnh sửa cuối:
Upvote 0
Tôi nghĩ là bài #50, #51, và #52 đều nhắm vào "Trung cấp".
Vì vậy tôi phê theo trình độ này, tức là cách thức code:

Bài #50:
Swicth là hàm để thay thế IF-THEN-ELSE và SELECT-CASE khi bạn cần viết code ngắn.
Càng nhiều tham số thì Switch càng mất hiệu quả. Bởi vì nó phải tính từng tham số.
Như bài trên dẫu tháng có phải là tháng hai hay không thì con toán tháng 2 vẫn phải tính.
Lệnh IF và Select nếu viết khéo thì gặp tháng <> 2 không phải làm tính.

Bài #51:
Tôi biết là bạn diễu, nhưng code như bài của bạn là loại code nguy hiểm.
Bạn tự mặc định UBound chỗ Dim mảng của bạn là 0. Điều này không luôn luôn đúng.
Gặp hệ thống định sẵn UBound là 1 thì code toi.
Muốn xài cocde này, đầu module phải có dòng "Option Base 0"

Bài #52:
Code này khai báo tham đàng hoàng nhưng để cho hàm mặc định là variant vô ích, chỉ kém hiệu quả thôi.
Function ABC(t&, n&)
Nên sửa lại là:
Function ABC&(t&, n&)
Và cũng giống như bài #50, không dùng hàm, cấu trúc dữ liệu của VBA, và rất ngắn gọn nhưng bị kém chỗ "tất cả các con toán đều phải tính".

Code không dùng IF, không Select, điển hình:
Function ToTiTe(byVal thang As Long, Optional Byval nam As Long = 0) As Long
On thang GoTo Thang1, Thang2, Thang3, Thang4, Thang5, Thang6, Thang7, Thang8, Thang9, Thang10, Thang11, Thang12
Exit Function
Thang1:
Thang3:
Thang5:
Thang7:
Thang8:
Thang10:
Thang12:
ToTiTe = 31
Exit Function
Thang4:
Thang6:
Thang9:
Thang11:
ToTiTe = 30
Exit Function
Thang2:
' code tính ngày ở đây
Exit Function
End Function

Code dài thòn, nhưng lệnh rẽ nhánh đến labels rất hiệu quả.
Nếu đặt ThangLon, ThangNho, Thang2 thì sẽ ngắn hơn, nhưng cái dòng On ... dễ nhầm lẫn hơn.
 
Upvote 0
Bài #51:
Tôi biết là bạn diễu, nhưng code như bài của bạn là loại code nguy hiểm.
Bạn tự mặc định UBound chỗ Dim mảng của bạn là 0. Điều này không luôn luôn đúng.
Theo tôi thấy thì code bạn này không màng đến Base 0 hay Base 1
Khai báo: Dim Th(12) As Integer
Nếu base 0 thì có 13 phần tử, nếu base 1 thì có 12 phần tử.
Dù vậy bạn ấy chỉ xài 12 phần tử từ 1 đến 12, phần tử 0 nếu có bị bỏ qua và không ảnh hưởng.
 
Upvote 0
Theo tôi thấy thì code bạn này không màng đến Base 0 hay Base 1
Khai báo: Dim Th(12) As Integer
Nếu base 0 thì có 13 phần tử, nếu base 1 thì có 12 phần tử.
Dù vậy bạn ấy chỉ xài 12 phần tử từ 1 đến 12, phần tử 0 nếu có bị bỏ qua và không ảnh hưởng.
Base 1 thì code chết tươi, bác ạ.
Ở nài #53, tôi nói là "Gặp hệ thống định sẵn UBound là 1 thì code toi."
Ở trình độ cdoe bậc trung thì tôi coi như đã biết qua dộ nguy hiểm của "mặc định", và biết tránh những tình huống ấy.

Mã:
      Th2(0) = 29 ' dòng này nè!
      Th2(1) = 28
      Th2(2) = 28
      Th2(3) = 28
 
      Th(2) = Th2(((Nam / 4) - (Nam \ 4)) * 4)
 
      Ngay1 = Th(Thg)
 
End Function
 
Upvote 0
Code bậc sơ đẳng:

Function ToTiTe(byVal thang As Long, Optional ByVal nam As Long = 1) As Long
ToTiTe = Day(DateSerial(nam, thang+1, 0))
End Function

Ghi thẳng vào ô, dùng hàm Evaluate và hàm bảng tính
 
Upvote 0
Bài #51:
Tôi biết là bạn diễu, nhưng code như bài của bạn là loại code nguy hiểm.
Bạn tự mặc định UBound chỗ Dim mảng của bạn là 0. Điều này không luôn luôn đúng.
Gặp hệ thống định sẵn UBound là 1 thì code toi.
Muốn xài cocde này, đầu module phải có dòng "Option Base 0"
lần đầu tiên e biết vụ Base 0 này luôn á ( e xin phép xưng e cho trẻ ^^ )
từ hồi sinh viên e đã gán thử giá trị cho phần tử thứ 0 ( bắt chước cách gán bên c dos ) , thấy không báo lỗi là xài cho hơn chục năm đi làm luôn, tính đến hiện tại chưa phải đền tiền vụ nào nên cứ thế xài thôi ^^


Theo tôi thấy thì code bạn này không màng đến Base 0 hay Base 1
Khai báo: Dim Th(12) As Integer
Nếu base 0 thì có 13 phần tử, nếu base 1 thì có 12 phần tử.
Dù vậy bạn ấy chỉ xài 12 phần tử từ 1 đến 12, phần tử 0 nếu có bị bỏ qua và không ảnh hưởng.

nói chung e mặc định trước giờ khai báo mảng trong vba sẽ được thêm 1 biến,
ví dụ : abc(12) -> sẽ được 13 biến tính từ 0 ^^

lúc đầu tính để như vầy, nhưng muốn tỏ ra nguy hiểm tí nên gán từ 0 á ^^
Th2(1) = 29
Th2(2) = 28
Th2(3) = 28
Th2(4) = 28

Th(2) = Th2(((Nam / 4) - (Nam \ 4)) * 4 + 1)

con toán \ lần đầu e mới biết luôn, trước giờ cứ phải viết như vầy Int(Nam / 4) @@
 
Lần chỉnh sửa cuối:
Upvote 0
PHP:
Function DaysOfMonth(ByVal Thg As Integer, ByVal Nam As Integer) As Integer
 DaysOfMonth = Choose(Thg, 31, IIf(Nam Mod 4 = 0, 29, 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
End Function
 
Upvote 0
chỉ là thêm điều kiện thôi 400, 100 thôi mà !


Mã:
Function Ngay1(ByVal Thg As Integer, ByVal Nam As Integer) As Integer
 
      Dim i As Integer
      Dim Th(12) As Integer
      Dim Th2(4) As Integer
      Dim Nam100(100) As Integer
      Dim Nam400(400) As Integer

      Th(1) = 31
      Th(2) = 0
      Th(3) = 31
      Th(4) = 30
      Th(5) = 31
      Th(6) = 30
      Th(7) = 31
      Th(8) = 31
      Th(9) = 30
      Th(10) = 31
      Th(11) = 30
      Th(12) = 31
 
      Th2(1) = 29
      Th2(2) = 28
      Th2(3) = 28
      Th2(4) = 28
 
      Th(2) = Th2(((Nam / 4) - (Nam \ 4)) * 4 + 1)
 
      '-------------100
      For i = 1 To 100
          Nam100(i) = 0
      Next i
      Nam100(1) = 1
 
      Th(2) = Th(2) - Nam100(((Nam / 100) - (Nam \ 100)) * 100 + 1)
 
 
      '-------------400
      For i = 1 To 400
          Nam400(i) = 0
      Next i
      Nam400(1) = 1
 
      Th(2) = Th(2) + Nam400(((Nam / 400) - (Nam \ 400)) * 400 + 1)
 
      '---------------
 
      Ngay1 = Th(Thg)
 
 
 
End Function


e xin phép chỉnh code bác 1 xíu !
PHP:
Function DaysOfMonth(ByVal Thg As Integer, ByVal Nam As Integer) As Integer
  Dim m2 As Integer
  m2 = IIf(Nam Mod 4 = 0, 29, 28)
  m2 = m2 - IIf(Nam Mod 100 = 0, 1, 0)
  m2 = m2 + IIf(Nam Mod 400 = 0, 1, 0)
  
  DaysOfMonth = Choose(Thg, 31, m2, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
End Function
 
Lần chỉnh sửa cuối:
Upvote 0
Web KT
Back
Top Bottom