Contents
Chào bạn đến với Fx Studio. Bài viết này vẫn là chủ đề liên quan tới các Operators trong thế giới RxSwift. Nhóm toán tử đề cập đến lần này là Combining Operators.
Dành một chút thời gian để quay về các khái niệm cơ bản của Operators trong RxSwift. Và dành cho các bạn mới tiếp cận thì có thể tham khảo lại link sau:
Chuẩn bị
-
- Xcode 11
- Swift 5
- Playground
Vẫn là em Playground huyền thoại. Chúng ta vẫn còn dùng tới nó để demo code cho bài viết này. Bạn chỉ cần tạo mới 1 file Playground từ project mà đã cài đặt ngay từ bài đầu của series. Bạn có thể checkout code lại đây.
Combining Operators
Bạn đã đi qua rất nhiều thứ trong RxSwift, từ bài đầu tiên trong series mệt mỏi này của Fx Studio rồi. Và mình có thể tóm gọn lại như sau:
Tạo & Lọc
Giờ chúng ta sẽ đi tới phần kết hợp. Có nhiều thứ chúng ta có thể kết hợp lại với nhau. Từ mặt dữ liệu tới đối tượng hoặc các stream … Đây cũng là những toán tử mà bạn sẽ phải dùng nhiều trong việc triển khai các logic trong project.
Chúng ta sẽ đi vào chi tiết các toán được sử dụng phổ biến với chức năng Combining.
1. Prefixing and concatenating
1.1. startWith

Đôi lúc chúng ta cần thêm một/nhiều phần tử trước khi Observable bắt đầu emit dữ liệu đi. Để làm gì, thì tuỳ ý bạn. Có khi đơn giản chỉ là muốn nó luôn có 1 giá trị nào đó lúc đầu tiên thôi. Toán tử này là startWith
Tham số truyền vào chính là giá trị mà có cùng kiểu giá trị với phần tử của Observable phát đi.
Ví dụ đoạn code sau:
let bag = DisposeBag()
Observable.of("B", "C", "D", "E")
.startWith("A")
.subscribe(onNext: { value in
print(value)
})
.disposed(by: bag)
Bạn sẽ thấy, với Observable phát ra các giá trị bắt đầu từ B. Thì để chèn thêm 1 phần tử ở trước ta dùng toán tử startWith và thêm A vào. Kết quả ra như sau:
A B C D E
Quá EZ!
1.2. Observable.concat
Trước tiên thì bạn hãy ví dụ cho toán tử mới này.
let bag = DisposeBag()
let first = Observable.of(1, 2, 3)
let second = Observable.of(4, 5, 6)
let observable = Observable.concat([first, second])
observable
.subscribe(onNext: { value in
print(value)
})
.disposed(by: bag)
Bạn sẽ nhận ra là Observable được tạo ra từ toán tử concat , thì có 1 tham số là 1 array Observable. Thử chạy kết quả xem như thế nào.
1 2 3 4 5 6
Tới đây thì bạn cũng hiểu ra rồi. concat sẽ nối các phần tử của nhiều sequence observable lại với nhau. Lại EZ nữa rồi!
1.3. concat
Bạn xem qua sơ đồ sau về toán tử Observable.concat.

Toán tử ở phần trên là một phương thức tạo Observable từ việc nối nhiều Observable lại với nhau. Còn đây là toán tử concat dùng với 1 đối tượng Observable. Nó sẽ nối các phần tử của một đối tượng Observable khác vào.
Phương pháp này giúp bạn có thêm 1 lúc nhiều phần tử, khắc phục nhược điểm của phần 1.
Code ví dụ như sau:
let bag = DisposeBag()
let first = Observable.of("A", "B", "C")
let second = Observable.of("D", "E", "F")
let observable = first.concat(second)
observable
.subscribe(onNext: { value in
print(value)
})
.disposed(by: bag)
Ta dùng first.concat với tham số là second. Kết quả in ra như sau:
A B C D E F
Cũng khá là đơn giản phải không nào. Đặc điểm của toán tử này là sử dụng cho 1 đối tượng Observable.
Chú ý: Toán tử
concatnày sẽ đợi Observable gốc hoàn thành. Thì sau đó Observable thứ 2 sẽ tiếp tục được nối vào.
1.4. concatMap
Lại là một toán tử concat đến từ nhóm toán tử Combining Operators. Và chúng ta xem sơ đồ mô tả nó như thế nào.

Khi có từ map trong tên của 1 toán tử, thì bạn cũng sẽ rằng sẽ có sự biến đổi về kiểu dữ liệu ở đây. Toán tử này đảm bảo việc các chuỗi được đóng lại trước khi chuỗi tiếp theo được đăng kí vào. Đảm vảo thứ tự trình tự.
Ở hình trên thì mô tả như sau:
- Hình tròn là 1 Observable
- Hình thoi là giá trị được phát ra cho Subscriber
- Luật trong toán tử là cứ 1 Observable sẽ phát ra 2 dữ liệu hình thoi
Còn sau đây là code ví dụ:
let bag = DisposeBag()
let cities = [ "Mien Bac" : Observable.of("Ha Noi", "Hai Phong"),
"Mien Trung" : Observable.of("Hue", "Da Nang"),
"Mien Nam" : Observable.of("Ho Chi Minh", "Can Tho")]
let observable = Observable.of("Mien Bac", "Mien Trung", "Mien Nam")
.concatMap { name in
cities[name] ?? .empty()
}
observable
.subscribe(onNext: { (value) in
print(value)
})
.disposed(by: bag)
Trong đó
citieslà một Dictionary với key là String và value là 1 Observable- Tạo ra 1 Observable với kiểu dữ liệu cho phần tử là
String - Dùng
concatMapđể biến đổi từStringthànhString. Tuy nhiên có sự can thiệp là nối các chuỗi thuộc value của Dictionary trên lại với nhau
Kết quả ra như sau:
Ha Noi Hai Phong Hue Da Nang Ho Chi Minh Can Tho
Cũng khá là không đơn giản phải không nào. Nói chung tụi này sẽ xoay vòng với nhau thôi. Chúng ta tiếp tục với nhóm tiếp theo.
2. Merging

Toán tử đầu tiên chính là merge. Cái tên cũng nói lên tất cả rồi. Và đặc điểm của toán tử này như sau:
mergesẽ tạo ra 1 Observable mới, khi một Observable có cácelementkiểu là Observable- Observable của merge sẽ kết thúc khi tất cả đều kết thúc
- Nó không quan tâm tới thứ tự các Observable được thêm vào. Nên nếu có bất cứ phần tử nào từ các Observable đó phát đi thì Subscriber cũng đều nhận được
Code ví dụ như sau:
let bag = DisposeBag()
let chu = PublishSubject<String>()
let so = PublishSubject<String>()
let source = Observable.of(chu.asObserver(), so.asObserver())
let observable = source.merge()
observable
.subscribe(onNext: { (value) in
print(value)
})
.disposed(by: bag)
chu.onNext("Một")
so.onNext("1")
chu.onNext("Hai")
so.onNext("2")
chu.onNext("Ba")
so.onCompleted()
so.onNext("3")
chu.onNext("Bốn")
chu.onCompleted()
Trong đó:
chu&solà 2 Subject, chịu trách nhiệm phát đi dữ liệusourceđược tạo ra từ 2 Observablechu&so. Nó là 1 Observableobservableđược tạo ra bằng toán tửmerge- Vẫn subscribe như bình thường
- Việc phát dữ liệu được thực hiện xen kẻ
Kết quả như sau:
Một 1 Hai 2 Ba Bốn
Và tất nhiên bạn có thể hạn chế được số lượng các Observable được phép merge vào thông qua tham số .merge(maxConcurrent:)
3. Combining elements
Phần này là sẽ tới các toán tử kết hợp trong nhóm Combining Operators. Tên của tụi nó lèn nhèn thật!
3.1. combineLatest

Thông qua sơ đồ mô tả toán tử combineLatest , toán tử này sẽ phát đi những giá trị là sự kết hợp của các cặp giá trị mới nhất của từng Observable. Để hình dung cụ thể, ta qua từng bước ví dụ sau đây:
- Tạo các Observable, sẽ phát dữ liệu đi. Có thể không theo thứ tự.
let chu = PublishSubject<String>()
let so = PublishSubject<String>()
- Sử dụng
combinedLatestvới 2 Observable trên. Sau đó tiến hànhsubscribenhư bình thường.
let observable = Observable.combineLatest(chu, so)
observable
.subscribe(onNext: { (value) in
print(value)
})
.disposed(by: bag)
- Giờ chúng ta phát dữ liệu đi một cách lần lượt và xem nó hoạt động như thế nào.
chu.onNext("Một")
chu.onNext("Hai")
so.onNext("1")
so.onNext("2")
chu.onNext("Ba")
so.onNext("3")
chu.onNext("Bốn")
so.onNext("4")
so.onNext("5")
so.onNext("6")
Kết quả sẽ khác dự đoán của bạn một chút.
("Hai", "1")
("Hai", "2")
("Ba", "2")
("Ba", "3")
("Bốn", "3")
("Bốn", "4")
("Bốn", "5")
("Bốn", "6")
Kết quả là sự kết hợp dữ liệu từ 2 dữ liệu được Observable chữ và số phát đi. Bạn sẽ thấy là không có ("Một", "1"). Vì lúc đó Observable so chưa phát ra gì cả. Khi so phát ra phần tử đầu tiên thì sẽ kết hợp với phần tử mới nhất của chu, đó là Hai.
Áp dụng tương tự cho các phần tử tiếp theo. Còn với complete thì như thế nào?
chu.onNext("Một")
chu.onNext("Hai")
so.onNext("1")
so.onNext("2")
chu.onNext("Ba")
so.onNext("3")
// completed
chu.onCompleted()
chu.onNext("Bốn")
so.onNext("4")
so.onNext("5")
so.onNext("6")
// completed
so.onCompleted()
Kết quả thực thi như sau:
("Hai", "1")
("Hai", "2")
("Ba", "2")
("Ba", "3")
("Ba", "4")
("Ba", "5")
("Ba", "6")
Vẫn không ảnh hưởng gì tới sự hoạt động của toán tử này. Nó sẽ vẫn lấy dữ liệu cuối cùng trước khi phát đi .onCompleted để kết hợp với các phần tử khác.
3.2. combineLatest(_:_:resultSelector:)
Tiếp tục toán tự combineLatest trong nhóm toán tử Combining Operators. Kết quả thực thi ở trên nhìn khá là xấu xí. Tuy nhiên, bạn có thể biến đổi nó như các toán tử map bằng cách sử dụng thêm tham số resultSelector và cung cấp 1 closure để biến đổi chúng.
Ta edit lại ví dụ trên một chút.
let observable = Observable.combineLatest(chu, so) { chu, so in
"\(chu) : \(so)"
}
Vì nếu không có tham số này, thì giá trị của toán tử là 1 Tuple kết hợp đơn giản mà thôi. Còn lại ta sử dụng tham số phụ này thì ta có quyền biến đổi thành kiểu dữ liệu mà ta mong muốn.
Kết quả như sau:
Hai : 1 Hai : 2 Ba : 2 Ba : 3 Ba : 4 Ba : 5 Ba : 6
Và đây là công thức chung cho bạn, nếu sau này muốn áp dụng nó. Bạn phải thêm filter để tăng tính đảm bảo.
let observable = Observable
.combineLatest(left, right) { ($0, $1) }
.filter { !$0.0.isEmpty }
Thêm 1 ví dụ nữa để thấy sự tiện lợi của toán tử này:
let choice: Observable<DateFormatter.Style> = Observable.of(.short, .long)
let dates = Observable.of(Date())
let observable = Observable.combineLatest(choice, dates) { format, when -> String in
let formatter = DateFormatter()
formatter.dateStyle = format
return formatter.string(from: when)
}
_ = observable.subscribe(onNext: { value in
print(value)
})
Với 1 giá trị Date bạn có thể lựa chọn kiểu format dữ liệu để in ra một cách tiện lợi nhất. Chúng ta không cần gọi hàm đi gọi hàm lại hay là for các kiểu nữa. Còn kết quả như sau:
8/21/20 August 21, 2020
Toán tử này có rất nhiều tuỳ chọn cho tham số của nó. Bạn hãy khám phá thêm.
3.3. zip
Nhóm toán tử Combining Operators này quá nhiều luôn. Chúng ta tiếp tục tìm hiểu về zip. Bạn hãy xem sơ đồ mô tả sau:

Khi bạn quan tâm tới thứ tự kết hợp theo đúng thứ tự phát đi của từng Observable. Nhưng combinedLatest không đáp ứng được thì zip sẽ giúp bạn hoàn thành tâm nguyện này.
Tất cả mọi thứ ví dụ đều như trên. Tuy nhiên, chỉ khác là các cặp giá trị kết hợp nhau thì sẽ theo thứ tự phát của các Observable. Xem qua lại ví dụ trên thì bạn sẽ hiểu thôi.
let observable = Observable.zip(chu, so) { chu, so in
"\(chu) : \(so)"
}
Vẫn là ví dụ lúc nãy, bạn chỉ cần edit lại thành là zip. Và tận hưởng kết quả nào!
Một : 1 Hai : 2 Ba : 3
Như vậy, là vừa đẹp vừa đúng rồi! Dù vậy, bạn cũng nên quản lý việc onCompleted của từng Observable trong đó, để đảm bảo dữ liệu như bạn mong muốn.
4. Trigger
Trigger sẽ được sử dụng, khi bạn muốn tạo ra 1 điều kiện nào đó từ Observable để một Observable khác được phép hoạt động.
4.1. withLatestFrom
Bắt đầu bằng ví dụ sau:

(Cái hình là mình mượn tạm để minh hoạ cho code ở đưới đây.)
let button = PublishSubject<Void>()
let textField = PublishSubject<String>()
let observable = button.withLatestFrom(textField)
_ = observable
.subscribe(onNext: { value in
print(value)
})
textField.onNext("Đa")
textField.onNext("Đà Na")
textField.onNext("Đà Nẵng")
button.onNext(())
button.onNext(())
Trong đó:
buttonlà một subject. VớiVoidthì chỉ phát ra sự kiện, chứ không có giá trị nào từonNexttextFieldlà một subject, phát ra cácStringobservablelà sự kết hợp củabuttonvớitextFieldthông qua toán tửwithLatestFrom- Mỗi lần
buttonphát đi tín hiệu, thì kết quả sẽ nhận được là phần tử mới nhất từtextField
Qua ví dụ trên cũng mô tả cho bạn thấy sự hoạt động của toán tử withLatestFrom rồi. Kết quả thực thi code như sau:
Đà Nẵng Đà Nẵng
4.2. sample
Chúng ta thay withLatestFrom bằng sample trong ví dụ trên.
let observable = textField.sample(button)
Kết quả sẽ cho ra 1 mà thôi.
Với sample là tương tự như withFromLatest. Nhưng nó chỉ phát khi Observable button phát ra.

Chú ý:
withLatestFromthì tham số là dữ liệu của một Observable khácsamplethì tham số là trigger từ một Observable khác
Chúng nó rất dễ nhầm lẫn, vì vậy bạn cần cẩn thận hơn khi sử dụng.
5. Switches
Các toán tử mới từ Combining Operators này, sẽ cho phép bạn tạo ra một Observable từ nhiều Observable khác. Mà bạn có thể quyết định được việc dữ liệu từ nguồn nào để các subscriber có thể nhận được.
5.1. amb

Đây là một toán tử khá là mơ hồ, cũng như cái tên của nó là ambiguity. Với các đặc tính sau:
- Nó sẽ tạo ra một Observable để giải quyết vấn đề quyết định nhận dữ liệu từ nguồn nào
- Trong khi cả 2 nguồn đều có thể phát dữ liệu. Thì nguồn nào phát trước, thì nó sẽ nhận dữ liệu từ nguồn đó.
- Nguồn phát sau sẽ bị âm thầm ngắt kết nối
Xem qua code demo cho thông não nè:
let bag = DisposeBag()
let chu = PublishSubject<String>()
let so = PublishSubject<String>()
let observable = chu.amb(so)
observable
.subscribe(onNext: { (value) in
print(value)
})
.disposed(by: bag)
so.onNext("1")
so.onNext("2")
so.onNext("3")
chu.onNext("Một")
chu.onNext("Hai")
chu.onNext("Ba")
so.onNext("4")
so.onNext("5")
so.onNext("6")
chu.onNext("Bốn")
chu.onNext("Năm")
chu.onNext("Sáu")
Kết quả thực thi ra như sau:
1 2 3 4 5 6
Vì so đã phát trước, nên các dữ liệu từ chu sẽ không nhận được. Nếu bạn cho thêm chu phát onNext trước số thì sẽ thấy dữ liệu nhận được sẽ toàn là từ chu.
5.2. switchLatest

Toán tử này tương tự như flatMapLatest trong bài viết trước. Và để thấu hiểu nó, thì mình sẽ đi qua ví dụ từng bước sau.
Đầu tiên, bạn cần tạo ra các Observable.
let chu = PublishSubject<String>() let so = PublishSubject<String>() let dau = PublishSubject<String>() let observable = PublishSubject<Observable<String>>()
Ta có:
- 3 subject thay nhau phát dữ liệu
observablevới kiểu dữ liệu phát đi làObsevable<String>, chính là kiểu của 3 subject trên
Tiến hành subscribe và dùng switchLatest như sau:
observable
.switchLatest()
.subscribe(onNext: { (value) in
print(value)
}, onCompleted: {
print("completed")
})
.disposed(by: bag)
Cũng khá là quen thuộc phải không nào! Giờ sang phần phát dữ liệu đi. Bạn xem tiếp.
observable.onNext(so)
so.onNext("1")
so.onNext("2")
so.onNext("3")
observable.onNext(chu)
chu.onNext("Một")
chu.onNext("Hai")
chu.onNext("Ba")
so.onNext("4")
so.onNext("5")
so.onNext("6")
observable.onNext(dau)
dau.onNext("+")
dau.onNext("-")
observable.onNext(chu)
chu.onNext("Bốn")
chu.onNext("Năm")
chu.onNext("Sáu")
Bạn sẽ thấy việc observable sẽ phát đi subject nào. Thì subscriber trên sẽ nhận được giá trị của subject đó. Kết quả thực thi như sau:
1 2 3 Một Hai Ba + - Bốn Năm Sáu
Đối với phần này rất dễ bị nhầm lẫn nên bạn muốn hiểu hơn, thì hãy thay phiên nhau việc phát dữ liệu.
Còn để kết thúc nó, thì phải phải .dispose subscription. Chứ không thể nào kết thúc nó được, mặc dù các subject có thể onCompleted hết tất cả nhưng nó vẫn không kết thúc.
6. Combining elements within a sequence
Tiếp theo là các toán tử của Combining Operators, với sự kết hợp các phần tử trong cùng 1 sequence observable. Có nhiều điểm thú vị với các toán tử này.
6.1. reduce
Đây là một toán tử khá là quen thuộc trong Swift code. Chắc bạn cũng vài lần sử dụng nó trong các Array để tính toán liên quan tới tất cả các phần tử trong array đó.
Ví dụ như cộng tất cả giá trị của một array lại với nhau. Hình sau đây cũng minh hoạ cho ví dụ này.

Về ví dụ bằng code thì như sau:
let source = Observable.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
let observable = source.reduce(0, accumulator: +)
_ = observable
.subscribe(onNext: { value in
print(value)
})
Trong đó accumulator là sự rút gọn toán tử + lại. Và 0 là giá trị ban đầu được cấp phát cho để thực hiện việc này.
Hoặc bạn có thể code xịn sò hơn như sau:
let observable = source.reduce(0) { $0 + $1 }
Hoặc code nghiêm chỉnh lại một chút.
let observable = source.reduce(0) { summary, newValue in
return summary + newValue
}
Việc tích luỹ sẽ bắt đầu bằng giá trị bạn cung cấp cho nó. Khi đó nó sẽ hiểu là $0. Tiếp theo khi nhận được 1 giá trị, thì giá trị đó là $1. Ta thực hiện biểu thức, kết quả trả về sẽ gán lại cho $0. Cứ như thế, vòng lặp định mệnh sẽ liếp tục cho đến hết. Tức là Observable phát đi giá trị cuối cùng được tính toán xong sau khi phát onCompleted.
OKAY phải không nào, EZ.
6.2. scan(_:accumulator:)
Thêm một thành viên hack não nữa từ nhóm toán tử Combining Operators. Bạn xem qua sơ đồ mô tả sau:

Về cấu trúc và cách viết thì tương tự reduce. Có chút khác biệt ở đây là, thay vì chờ Observable kết thúc và đưa ra kết quả cuối cùng. Thì scan nó sẽ tính toán và phát đi từng kết quả tinh toán được, sau khi có dữ liệu từ Observable phát ra. Không quan tâm Observable kết thúc mới thực hiện.
Chỉ cần dùng lại ví dụ trên, thay reduce bằng scan là bạn sẽ hiểu thôi.
let source = Observable.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
//let observable = source.scan(0, accumulator: +)
//let observable = source.scan(0) { $0 + $1 }
let observable = source.scan(0) { summary, newValue in
return summary + newValue
}
_ = observable
.subscribe(onNext: { value in
print(value)
})
Kết quả thực thi như sau:
1 3 6 10 15 21 28 36 45
Về bản chất thì không khác nhau mấy khi so với reduce. Về ý nghĩa thì bạn sẽ có 1 cách tạo ra 1 for loop xịn sò nữa. Ahihi!
Tạm kết
Mình sẽ tóm tắt lại ý nghĩa các toán tử trong nhóm Combining Operators nha.
startWithđể thêm một hoặc nhiều phần tử trước khi Observable phát ra giá trị đầu tiên.concatdùng để nối nhiều Observables lại với nhau. Hoặc tiếp tục việc phát dữ liệu sau khi một Observable kết thúc bằng 1 Observable khác.concatMapvừa nối vừa biến đổimergehợp nhất các giá trị từ nhiều Observables phát ra (không theo thứ tự) thành 1 Observable duy nhất.combineLatestkết hợp các cặp giá trị mới nhất từ các Observables. Và không quan tâm tới thứ tự phát.ziptương tự như combineLatest nhưng cặp giá trị kết hợp sẽ theo thứ tự phát của các Observables.withLatestFromlà trigger với tham số là dữ liệu của một Observable khác. Nó phát và nhận giá trị từ người khác.samplecũng là trigger, nhưng tham số là một Observable khác. Khi người khác phát thì nó sẽ phát ra dữ liệu.ambgiải quyết việc chọn Observables nào sẽ toàn quyền phát dữ liệu. Bằng cách xem Observable nào phát trước tiên, các Observable còn lại thì sẽ bị ngắt kết nối.switchLatestthì chỉ nhận dữ liệu phát ra từ Observable cuối cùng tham gia vào.reducelàm gọn lại tất cả các dữ liệu phát ra từ Observable bằng một luật tính toán nào đó. Khi Observable kết thúc thì sẽ nhận được kết quả. Ngoài ra, subscriber sẽ không nhận được bất kì giá trị nào khác.scantương tự như reduce. Nhưng subscriber sẽ nhận được kết quả ở từng bước tính toán khi Observable phát dữ liệu đi.
Cảm ơn bạn đã đọc bài viết này!
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Multi-Layer Prompt Architecture – Chìa khóa Xây dựng Hệ thống AI Phức tạp
- Khi “Prompt Template” Trở Thành Chiếc Hộp Pandora
- Vòng Lặp Ảo Giác
- Giàn Giáo Nhận Thức (Cognitive Scaffold) trong Prompt Engineering
- Bản Thể Học (Ontology) trong Prompt Engineering
- Hướng Dẫn Vibe Coding với Gemini CLI
- Prompt Bản Thể Học (Ontological Prompt) và Kiến Trúc Nhận Thức (Cognitive Architecture Prompt) trong AI
- Prompt for Coding – Code Translation Nâng Cao & Đối Phó Rủi Ro và Đảm Bảo Chất Lượng
- Tại sao cần các Chiến Lược Quản Lý Ngữ Cảnh khi tương tác với LLMs thông qua góc nhìn AI API
- Prompt for Coding – Code Translation với Kỹ thuật Exemplar Selection (k-NN)
Archives
- October 2025 (1)
- September 2025 (4)
- August 2025 (5)
- July 2025 (10)
- June 2025 (1)
- May 2025 (2)
- April 2025 (1)
- March 2025 (8)
- January 2025 (7)
- December 2024 (4)
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)

