Bài 29.3: Tạo email từ chuỗi trong C++

Chào mừng các bạn quay trở lại với chuỗi bài blog về C++!

Trong thế giới lập trình, việc làm việc với dữ liệu dưới dạng văn bản, đặc biệt là các chuỗi ký tự, là vô cùng phổ biến. Một trong những loại dữ liệu có cấu trúc thường gặp nhất được biểu diễn dưới dạng chuỗi chính là địa chỉ email. Từ việc thu thập thông tin người dùng đến xử lý dữ liệu danh bạ, chúng ta đều cần biết cách tạo, kiểm tratrích xuất thông tin từ các chuỗi email.

Bài viết hôm nay, Bài 29.3, sẽ đưa chúng ta đi sâu vào cách thực hiện những thao tác này với chuỗi trong C++. Chúng ta sẽ không chỉ học cách kết hợp các phần lại để tạo ra một địa chỉ email hoàn chỉnh, mà còn tìm hiểu cách kiểm tra xem một chuỗi có trông giống định dạng email hay không, và làm sao để tách username và domain ra khỏi chuỗi đó.

Hãy cùng bắt đầu nhé!

Địa chỉ Email - Cấu trúc cơ bản dưới dạng chuỗi

Một địa chỉ email thông thường có cấu trúc username@domain.tld. Ví dụ: tennguoidung@tencongty.com. Trong C++, chúng ta biểu diễn địa chỉ email này bằng một đối tượng **string**.

#include <iostream>
#include <string>

int main() {
    string email_address = "example.user@mail.com";
    cout << "Địa chỉ email: " << email_address << endl;
    return 0;
}

Đoạn code đơn giản trên chỉ cho thấy cách lưu trữ một chuỗi email. Tuy nhiên, làm thế nào để tạo chuỗi này từ các phần riêng lẻ, hoặc phân tích nó ra? Đó là nội dung chính của bài này.

1. Tạo chuỗi email từ các phần riêng lẻ

Thường thì chúng ta không có sẵn chuỗi email hoàn chỉnh mà cần xây dựng nó từ các thông tin đã có, ví dụ như tên người dùng và tên miền. Trong C++, cách đơn giản nhất để kết hợp các chuỗi lại với nhau là sử dụng toán tử cộng (+).

Giả sử bạn có tên người dùng là "nguyenvana" và tên miền là "gmail.com". Bạn muốn tạo địa chỉ email nguyenvana@gmail.com.

#include <iostream>
#include <string>

int main() {
    string username = "nguyenvana";
    string domain = "gmail.com";
    string at_symbol = "@";

    // Kết hợp các phần lại với nhau
    string email = username + at_symbol + domain;

    cout << "Username: " << username << endl;
    cout << "Domain: " << domain << endl;
    cout << "Địa chỉ email tạo ra: " << email << endl;

    return 0;
}

Giải thích:

  • Chúng ta khai báo ba chuỗi riêng biệt: username, domain, và ký tự @.
  • Sử dụng toán tử +, chúng ta nối username, at_symbol, và domain theo đúng thứ tự để tạo thành chuỗi email hoàn chỉnh.
  • string trong C++ hỗ trợ quá tải (overload) toán tử + để thực hiện phép nối chuỗi một cách trực quan.

Bạn cũng có thể kết hợp nhiều phần hơn. Ví dụ, tạo email từ họ, tên, và tên miền:

#include <iostream>
#include <string>

int main() {
    string first_name = "le";
    string last_name = "thi";
    string middle_name = "b";
    string domain = "example.net";

    // Tạo username có dạng letb.nguyen
    string username = first_name + middle_name + "." + last_name;

    // Tạo email hoàn chỉnh
    string email = username + "@" + domain;

    cout << "First Name: " << first_name << endl;
    cout << "Last Name: " << last_name << endl;
    cout << "Middle Name: " << middle_name << endl;
    cout << "Domain: " << domain << endl;
    cout << "Username tạo ra: " << username << endl;
    cout << "Địa chỉ email tạo ra: " << email << endl;

    return 0;
}

Trong ví dụ này, chúng ta tạo username bằng cách nối tên và thêm dấu chấm, sau đó mới nối username với @domain để có email cuối cùng. Toán tử + cho phép bạn nối nhiều chuỗi hoặc ký tự (char) lại với nhau trong một biểu thức duy nhất.

2. Kiểm tra định dạng cơ bản của chuỗi email

Sau khi tạo (hoặc nhận được) một chuỗi email, chúng ta thường cần kiểm tra xem nó có tuân thủ định dạng cơ bản của một địa chỉ email hay không. Điều này giúp tránh xử lý các dữ liệu rác hoặc sai cấu trúc.

Quan trọng: Kiểm tra định dạng email đầy đủ và chính xác theo tiêu chuẩn RFC là một nhiệm vụ phức tạp, thường yêu cầu sử dụng Biểu thức chính quy (Regular Expressions). Trong bài này, chúng ta sẽ chỉ thực hiện một số kiểm tra cơ bản nhất bằng các hàm xử lý chuỗi của string.

Các kiểm tra cơ bản có thể bao gồm:

  1. Chuỗi có chứa ký tự @ không?
  2. Chuỗi có chứa ký tự . không?
  3. Ký tự @ có xuất hiện trước ký tự . đầu tiên không?
  4. Ký tự @ có phải là ký tự đầu tiên hoặc cuối cùng không? (Tương tự với .)

Chúng ta có thể sử dụng hàm **string::find** để tìm vị trí của một ký tự hoặc một chuỗi con trong chuỗi. Hàm này trả về chỉ số (index) của lần xuất hiện đầu tiên, hoặc **string::npos** nếu không tìm thấy.

Hãy xây dựng một hàm kiểm tra cơ bản:

#include <iostream>
#include <string>

// Hàm kiểm tra định dạng email cơ bản
bool isValidEmailBasic(const string& email) {
    // 1. Kiểm tra xem có chứa '@' không và không phải ở đầu/cuối
    size_t at_pos = email.find('@');
    if (at_pos == string::npos || at_pos == 0 || at_pos == email.length() - 1) {
        return false;
    }

    // 2. Kiểm tra xem có chứa '.' sau '@' không
    // Tìm dấu '.' đầu tiên SAU vị trí của '@'
    size_t dot_pos = email.find('.', at_pos + 1);
    if (dot_pos == string::npos || dot_pos == email.length() - 1) {
        return false;
    }

    // 3. Kiểm tra xem có ký tự nào giữa '@' và '.' đầu tiên sau '@' không
    // Nghĩa là, dot_pos phải lớn hơn at_pos + 1
    if (dot_pos <= at_pos + 1) {
         return false; // Tránh các trường hợp như "a@.com" hoặc "a@."
    }

    // Có thể thêm các kiểm tra khác như không có ".." hoặc "@."
    // Nhưng với mục đích cơ bản, các kiểm tra trên là đủ cho ví dụ.

    return true; // Vượt qua các kiểm tra cơ bản
}

int main() {
    string email1 = "test@example.com";
    string email2 = "invalid-email"; // Thiếu '@'
    string email3 = "user@domain"; // Thiếu '.' sau '@'
    string email4 = "@domain.com"; // '@' ở đầu
    string email5 = "user@domain."; // '.' ở cuối sau '@'
    string email6 = "user@.com"; // Không có ký tự giữa '@' và '.'

    cout << email1 << " is valid basic? " << (isValidEmailBasic(email1) ? "Yes" : "No") << endl;
    cout << email2 << " is valid basic? " << (isValidEmailBasic(email2) ? "Yes" : "No") << endl;
    cout << email3 << " is valid basic? " << (isValidEmailBasic(email3) ? "Yes" : "No") << endl;
    cout << email4 << " is valid basic? " << (isValidEmailBasic(email4) ? "Yes" : "No") << endl;
    cout << email5 << " is valid basic? " << (isValidEmailBasic(email5) ? "Yes" : "No") << endl;
    cout << email6 << " is valid basic? " << (isValidEmailBasic(email6) ? "Yes" : "No") << endl;


    return 0;
}

Giải thích:

  • Hàm isValidEmailBasic nhận một chuỗi email dưới dạng tham chiếu hằng (const string&) để tránh sao chép và đảm bảo không thay đổi chuỗi gốc.
  • email.find('@'): Tìm vị trí đầu tiên của ký tự @. Nếu không tìm thấy, nó trả về string::npos.
  • Chúng ta kiểm tra nếu at_posstring::npos hoặc @ ở vị trí 0 hoặc @ ở vị trí cuối cùng (email.length() - 1), thì chuỗi không hợp lệ cơ bản và trả về false.
  • email.find('.', at_pos + 1): Tìm vị trí đầu tiên của ký tự . bắt đầu tìm từ vị trí ngay sau @. Điều này đảm bảo dấu chấm chúng ta tìm thấy nằm trong phần domain.
  • Tương tự, chúng ta kiểm tra dot_pos. Nếu không tìm thấy dấu chấm sau @ hoặc dấu chấm đó ở vị trí cuối cùng của chuỗi, trả về false.
  • Kiểm tra dot_pos <= at_pos + 1 đảm bảo rằng phải có ít nhất một ký tự giữa @ và dấu . đầu tiên trong domain.
  • Nếu tất cả các kiểm tra này đều vượt qua, chúng ta coi chuỗi đó là có định dạng email cơ bản và trả về true.

Hàm này chỉ là bước kiểm tra ban đầu. Một địa chỉ email hợp lệ có thể chứa các ký tự đặc biệt, dấu gạch ngang, số, v.v., theo các quy tắc phức tạp hơn. Đối với các ứng dụng thực tế yêu cầu độ chính xác cao, bạn nên tìm hiểu về Regular Expressions trong C++ (thư viện <regex>).

3. Trích xuất Username và Domain

Khi bạn có một chuỗi email, bạn có thể muốn tách nó thành hai phần chính: username (phần trước @) và domain (phần sau @). Chúng ta có thể làm điều này bằng cách sử dụng hàm **string::substr** kết hợp với **string::find** để xác định vị trí của ký tự @.

Hàm substr(pos, len) của string trả về một chuỗi con bắt đầu từ vị trí pos và có độ dài len. Nếu bỏ qua len, nó sẽ trả về chuỗi con từ pos đến hết chuỗi gốc.

#include <iostream>
#include <string>

int main() {
    string email = "user.name123@sub.domain.co.uk";

    // Tìm vị trí của ký tự '@'
    size_t at_pos = email.find('@');

    // Kiểm tra xem '@' có tồn tại không
    if (at_pos != string::npos) {
        // Trích xuất Username (từ đầu chuỗi đến vị trí '@')
        string username = email.substr(0, at_pos);

        // Trích xuất Domain (từ vị trí sau '@' đến hết chuỗi)
        string domain = email.substr(at_pos + 1);

        cout << "Chuỗi email gốc: " << email << endl;
        cout << "Username: " << username << endl;
        cout << "Domain: " << domain << endl;

    } else {
        cout << "Chuỗi \"" << email << "\" không phải là định dạng email hợp lệ (thiếu '@')." << endl;
    }

    return 0;
}

Giải thích:

  • Chúng ta dùng email.find('@') để lấy vị trí của ký tự @.
  • Nếu at_pos không phải là string::npos (nghĩa là @ có tồn tại trong chuỗi):
    • email.substr(0, at_pos): Tạo chuỗi con bắt đầu từ vị trí 0 (đầu chuỗi) và có độ dài bằng at_pos. Đây chính là phần username.
    • email.substr(at_pos + 1): Tạo chuỗi con bắt đầu từ vị trí at_pos + 1 (vị trí ngay sau @) và lấy tất cả các ký tự còn lại đến hết chuỗi. Đây chính là phần domain.
  • Nếu @ không được tìm thấy, chúng ta in ra thông báo lỗi.

Ví dụ khác với chuỗi email đơn giản hơn:

#include <iostream>
#include <string>

int main() {
    string email = "admin@website.org";
    size_t at_pos = email.find('@');

    if (at_pos != string::npos) {
        string username = email.substr(0, at_pos);
        string domain = email.substr(at_pos + 1);

        cout << "Email: " << email << endl;
        cout << "Username: " << username << endl;
        cout << "Domain: " << domain << endl;
    } else {
        cout << "Chuỗi \"" << email << "\" không chứa '@'." << endl;
    }

    return 0;
}

Kỹ thuật sử dụng findsubstr không chỉ áp dụng cho email mà còn rất hữu ích để phân tích các chuỗi có định dạng nhất định, sử dụng các ký tự phân cách (delimiter) như dấu phẩy (,), dấu hai chấm (:), dấu gạch ngang (-), v.v.

Bài tập ví dụ: C++ Bài 18.A3: Tránh tiếp xúc

Tránh tiếp xúc

FullHouse Dev đang quản lý một ký túc xá có N phòng xếp thành một hàng thẳng. Họ cần bố trí chỗ ở cho X người, trong đó có Y người bị nhiễm thủy đậu. Để đảm bảo an toàn, họ phải tuân thủ các quy tắc sau:

  • Không ai được ở trong phòng kề cạnh phòng của người bị nhiễm thủy đậu.
  • Hai người bị nhiễm thủy đậu không thể ở trong hai phòng kề nhau.

Hãy giúp FullHouse Dev tìm ra giá trị N nhỏ nhất để có thể bố trí chỗ ở cho tất cả mọi người theo quy tắc trên.

INPUT FORMAT

  • Dòng đầu tiên chứa số nguyên T — số lượng bộ test.
  • Mỗi bộ test gồm một dòng chứa hai số nguyên X và Y — tổng số người và số người bị nhiễm thủy đậu.

OUTPUT FORMAT

  • Với mỗi bộ test, in ra một số nguyên duy nhất — giá trị N nhỏ nhất thỏa mãn yêu cầu.

CONSTRAINTS

  • 1 ≤ T ≤ 200
  • 1 ≤ X ≤ 1000
  • 0 ≤ Y ≤ X
Ví dụ

Input

3
4 0
5 3
3 3

Output

4
8
5
Giải thích:
  • Test 1: Cần ít nhất 3 phòng để bố trí 1 người bệnh và 1 người khỏe: [H, B, H] (H: người khỏe, B: người bệnh).
  • Test 2: Cần ít nhất 7 phòng: [H, B, H, H, H, B, H].
  • Test 3: Cần ít nhất 14 phòng: [H, B, H, H, H, B, H, H, H, B, H, H, H, H].
  • Test 4: Cần ít nhất 28 phòng để bố trí 5 người bệnh và 15 người khỏe theo quy tắc. ```cpp #include <iostream> #include <cmath> // For ceil

int main() { // Sử dụng stdio_sync_with_stdio(false) và cin.tie(NULL) để tăng tốc độ đọc/ghi ios_base::sync_with_stdio(false); cin.tie(NULL);

int T;
cin >> T; // Đọc số lượng bộ test

while (T--) {
    int X, Y;
    cin >> X >> Y; // Đọc tổng số người (X) và số người nhiễm thủy đậu (Y)

    int N; // Số phòng nhỏ nhất cần tìm

    if (Y == 0) {
        // Trường hợp 1: Không có người bị nhiễm thủy đậu (Y=0).
        // Tất cả X người đều khỏe mạnh. Không có ràng buộc về phòng kề cạnh.
        // Mỗi người khỏe cần 1 phòng. Số phòng tối thiểu N chính bằng số người X.
        N = X;
    } else if (Y == X) {
        // Trường hợp 2: Tất cả X người đều bị nhiễm thủy đậu (Y=X).
        // Ràng buộc: Hai người bị nhiễm thủy đậu không thể ở phòng kề nhau.
        // Ràng buộc "Không ai được ở phòng kề cạnh phòng của người bị nhiễm"
        // áp dụng cho trường hợp này có nghĩa là phòng kề người bệnh phải trống.
        // Để sắp xếp Y người bệnh với số phòng tối thiểu theo quy tắc,
        // họ phải cách nhau ít nhất 1 phòng trống: B E B E B ... E B
        // Chuỗi này có Y người bệnh (B) và Y-1 phòng trống (E) ở giữa.
        // Tổng số phòng cần thiết là Y + (Y-1) = 2Y - 1.
        N = 2 * Y - 1;
    } else {
        // Trường hợp 3: Có cả người khỏe và người bệnh (0 < Y < X).
        // Y người bệnh, X-Y người khỏe.
        // Ràng buộc chính: Phòng kề cạnh người bệnh phải trống.
        // Để xếp Y người bệnh và X-Y người khỏe với số phòng tối thiểu,
        // ta xếp Y người bệnh sao cho các phòng kề họ là trống, và xếp X-Y người khỏe vào các phòng còn lại không kề người bệnh.
        // Cấu trúc tối thiểu cho Y người bệnh để phòng kề họ trống là E B E B E ... E B E.
        // Cấu trúc này gồm Y vị trí cho người bệnh (B) và Y+1 vị trí trống bắt buộc (E) ở giữa và hai đầu.
        // Tổng cộng 2Y + 1 phòng cho cấu trúc cơ bản của người bệnh và phòng trống bắt buộc.
        // Y vị trí B được người bệnh chiếm. Y+1 vị trí E bắt buộc phải trống (không thể xếp người khỏe vào đây vì kề B).
        // X-Y người khỏe cần X-Y phòng riêng biệt.
        // Họ phải được xếp vào các phòng khác ngoài 2Y+1 phòng của cấu trúc E B E... E B E.
        // Tuy nhiên, các ví dụ cho thấy có thể sắp xếp tối ưu hơn bằng cách phân bố người khỏe
        // vào các vị trí trống không kề người bệnh trong tổng số phòng.
        // Số phòng tối thiểu có thể được tính dựa trên số người bệnh và số người khỏe còn lại.
        // Với Y người bệnh (Y > 0) và X-Y người khỏe (X-Y > 0):
        // Nếu Y = 1: Cần 1 người bệnh (B) và X-1 người khỏe (H).
        // Cấu trúc tối thiểu: H...H E B E.
        // Gồm X-1 phòng cho người khỏe, 1 phòng E ngăn cách, 1 phòng B, 1 phòng E kề B.
        // Tổng số phòng = (X-1) + 1 + 1 + 1 = X+2.
        if (Y == 1) {
            N = X + 2;
        } else {
            // Nếu Y > 1 và 0 < Y < X. Y người bệnh, X-Y người khỏe.
            // Cần xếp Y người bệnh và X-Y người khỏe.
            // Y người bệnh cần các phòng cách nhau bởi E. Chuỗi B E B E ... E B cần 2Y-1 phòng.
            // Số người khỏe X-Y cần được xếp vào các phòng không kề B.
            // Có Y-1 khoảng trống E giữa các B trong chuỗi B E B ... E B.
            // Người khỏe có thể được xếp vào các khoảng trống này nếu chúng đủ lớn (ít nhất 2 phòng E liên tiếp).
            // Hoặc xếp H ở hai đầu chuỗi B E ... E B, cách B bằng E.
            // Số phòng cần thêm ngoài X phòng cho người (X người, mỗi người 1 phòng) phụ thuộc vào Y.
            // Công thức tổng quát cho trường hợp Y > 1 và X > Y, dựa trên phân tích số phòng trống bắt buộc
            // và khả năng xếp người khỏe vào các khoảng trống, được suy ra là:
            // N = X + (Y - 1) + ceil((X - Y) / (Y - 1)).
            // Trong đó:
            // X là số phòng cho X người.
            // Y-1 là số phòng trống tối thiểu giữa Y người bệnh trong chuỗi B E ... E B.
            // ceil((X-Y)/(Y-1)) là số lượng phòng trống thêm cần thiết để phân bổ X-Y người khỏe vào Y-1 khoảng trống giữa người bệnh và hai đầu dãy, đảm bảo cách ly.
            // Sử dụng phép chia số nguyên để tính ceil(A/B) cho A>=0, B>0 là (A+B-1)/B.
            // A = X-Y (số người khỏe), B = Y-1 (số khoảng trống giữa Y người bệnh).
            // Cả X-Y >= 1 và Y-1 >= 1 trong trường hợp này.
            N = X + (Y - 1) + ((X - Y) + (Y - 1) - 1) / (Y - 1);
        }
    }
    cout << N << endl; // In kết quả N cho mỗi bộ test
}

return 0;

} ```

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.