• Chủ Nhật, 20/03/2011 08:14 (GMT+7)

    Trả lời thư bạn đọc

    TS. Nguyễn Văn Hiệp
    Mục giải đáp thắc mắc của các bạn đọc do TS. Nguyễn Văn Hiệp phụ trách.

    Câu hỏi:
    File DBF của Foxpro; SQL bị hư/lỗi do máy tắt đột ngột, xin hỏi có phần mềm nào hỗ trợ sửa file hiệu quả không ?

    Trả lời:
    File database *.dbf của Foxpro; *.mdf của SQL (hay bất kỳ file dữ liệu của ứng dụng nào khác) có thể bị hư trong quá trình sử dụng. Có nhiều nguyên nhân làm file bị hư, thí dụ, bị virus phá, máy bị dừng đột ngột khi bạn đang làm việc với file, chương trình thực hiện hiệu chỉnh sai nội dung… Cách khắc phục là dùng tiện ích sửa lỗi phù hợp để sửa lỗi file *.dbf, một trong các trình sửa lỗi có thể dùng được là tiện ích "DBF Doctor" của hãng AsterSoft. Lưu ý, đây là tiện ích được thương mại hóa nên bạn phải mua mới dùng tốt, còn nếu bạn download trên mạng về dùng thì thường đó chỉ là bản dùng thử, không có đủ khả năng để sửa tất cả lỗi. Ngay cả khi bạn dùng bản chính thức (của bất kỳ hãng nào) thì tỉ lệ sửa lỗi cũng tương đối, vẫn có nhiều trường hợp sửa không được. Do đó cách tốt nhất vẫn là "phòng bệnh hơn chữa bệnh", bạn nên "backup" file database của mình từ đĩa cứng lên môi trường lưu trữ khác như USB, CD theo định kỳ để nếu file trên đĩa cứng bị hư hay khi cần thiết, bạn dùng lại file backup.

    Câu hỏi:
    Xin hỏi cách khai báo danh sách liên kết đơn không có ô đầu mục trong cấu trúc dữ liệu.

    Trả lời:
    Như bạn đã biết, hiện nay các thuật ngữ tiếng Việt miêu tả các khái niệm trong lĩnh vực tin học chưa được chuẩn hóa, mỗi người mỗi kiểu nên dễ làm cho người đọc hiểu lầm, hiểu sai và thậm chí không hiểu được. Thí dụ trong câu hỏi của bạn có thuật ngữ "ô đầu mục", lúc đầu chúng tôi cũng không hiểu ý muốn nói gì. Sau khi truy tìm trên mạng, chúng tôi thấy thuật ngữ này được dùng trong bài giảng môn "Cấu trúc dữ liệu" của BM. Khoa học Máy tính Trường Đại học Hàng hải.

    Trước hết, chúng tôi xin tóm tắt lại nội dung giới thiệu về danh sách liên kết đơn trong bài giảng: Danh sách liên kết đơn là một trong nhiều cách khác nhau để miêu tả danh sách nhiều phần tử dữ liệu có cấu trúc giống nhau và có mối quan hệ với nhau mà không đòi hỏi các phần tử này nằm liên tục nhau. Để cài đặt danh sách liên kết đơn, ta cài đặt mỗi phần tử trong danh sách thành 1 ô (item), mỗi ô là 1 mẫu tin (record, structure) có 2 trường (field) thông tin chức năng: Trường "value" chứa giá trị của phần tử trong danh sách, trường "next" là một con trỏ (pointer) giữ địa chỉ của ô kế tiếp. Trường "next" của phần tử cuối trong danh sách chỉ đến một giá trị đặc biệt là NULL. Sau đây là hình minh họa sự cài đặt 1 danh sách liên kết đơn có n phần tử (dùng n ô miêu tả n phần tử thật của danh sách và 1 ô khác (ô đầu mục) để miêu tả đầu danh sách):



    Thật ra, thường không ai cài đặt y như hình trên, vì như vậy là không cần thiết và không hiệu quả. Chúng ta sẽ cài đặt theo hình sau:

    Sự khác nhau giữa 2 hình trên là thay vì ta phải tốn 1 ô đầu mục để chứa địa chỉ phần tử đầu danh sách, ta chỉ tốn 1 biến pointer (dài 4 byte trong ứng dụng chạy trên Windows hay Linux). Chúng tôi dùng ngôn ngữ C++ (ngôn ngữ được dùng trong bài giảng kể trên) để thể hiện danh sách liên kết đơn theo tinh thần của hình trên như sau:



    //định nghĩa kiểu của từng item (ô)
    #define NULL 0
    typedef struct Element {
    int value; //giả sử nội dung phần tử là số nguyên
    Element* next; //trỏ tới phần tử kết tiếp
    } Element;
    //định nghĩa biến miêu tả header của danh sách
    Element* head=NULL;
    //tạo n phần tử từ an đến a1 và thêm vào đầu danh sách liên kết
    int i;
    int n = 10;
    Element* item;
    for (i=n; i >0; i--) {
    item = new Element;
    item->value = i;
    item->next = head;
    head = item;
    }

    Câu hỏi:
    Máy tính chơi game bị hiện tượng gãy hình, phải bật chức năng vsync trong driver mới hết, nhưng như vậy tốc độ game bị giảm bớt. Xin hỏi cách khắc phục.

    Trả lời:
    Lỗi mà bạn miêu tả thuộc về device driver của card màn hình và/hoặc phần cứng card. Bạn hãy thử vào website của hãng sản xuất card màn hình để biết thông tin cụ thể và chi tiết về cách giải quyết (thường là cập nhật phiên bản mới hơn của device driver).

    Câu hỏi:
    Tôi làm tại phòng KHTH của bệnh viện nhi Hải Dương mới thành lập, chúng tôi muốn dùng CNTT quản lý dữ liệu thay cho sổ sách giấy tờ, mong nhận được các giải pháp cụ thể của các chuyên gia!

    Trả lời:
    Ý tưởng của bạn là rất tiến bộ. Thật vậy, hầu hết các công việc nghiệp vụ đời thường như quản lý nhân viên, bệnh nhân, tài sản,… đều có thể tin học hóa. Hơn nữa, các hệ thống nghiệp vụ chạy trên máy tính thường có nhiều ưu điểm vượt trội so với hệ thống thủ công truyền thống. Tuy nhiên, theo chúng tôi biết hiện nay nhà nước chưa công nhận toàn bộ hệ thống nghiệp vụ được tin học hóa nào, chúng ta vẫn phải kết hợp cả 2 phía: máy tính và hệ thống tin học hóa sẽ thực hiện hầu hết mọi hoạt động nghiệp vụ, dữ liệu nhập/xuất vẫn phải tồn tại trên giấy tờ, được xác nhận/đóng dấu bởi người có trách nhiệm và được lưu trữ theo qui định pháp luật.

    Do qui trình nghiệp vụ chưa được chuẩn hóa, mỗi cơ quan, bệnh viện có những mức độ, yêu cầu tin học hóa khác nhau nên nếu muốn tin học hóa các công việc nghiệp vụ ở bệnh viện mình, bạn có thể liên hệ một công ty phần mềm uy tín chuyên về quản lý nghiệp vụ để trao đổi, bàn bạc và nhờ họ tư vấn cụ thể về từng bài toán nghiệp vụ cần tin học hóa. Bạn cũng có thể liên hệ các bệnh viện Nhi khác trong nước (số lượng rất ít) để biết mức độ tin học hóa cũng như kinh nghiệm sử dụng các hệ thống tin học hóa của họ.

    Câu hỏi:
    Không biết máy tính của tôi hiện đang nhiễm virus gì mà khi mở một văn bản Word hay Excel đã lưu để làm tiếp thì không lưu được nữa mà lại chuyển sang tập tin *.tmp.

    Trả lời:
    Có nhiều người thắc mắc như bạn, nhưng vì họ chưa nhờ chuyên gia xử lý virus tới khảo sát trực tiếp máy tính để xác định chính xác nguyên nhân có phải do virus không, nếu đúng là virus thì virus nào đã gây ra hiện tượng lỗi này?
    Lưu ý, trong khi người dùng soạn thảo file Word, ứng dụng Word sẽ tạo nhiều file tạm *.tmp để lưu giữ những kết quả trung gian phục vụ lệnh Undo của người dùng (có thể 1 hay nhiều lần). Trước khi Word dừng chạy, nó sẽ xóa các file *.tmp mà nó đã tạo ra để người dùng không biết gì về các file tạm này. Tuy nhiên nếu Word bị ngưng đột ngột (do mất điện, máy hư, virus phá,...), nó không thể kịp xóa các file tạm và người dùng sẽ thấy các file này sau đó. Nếu thấy bất tiện, người dùng có thể xóa chúng đi.

    Còn nếu thất bại trong việc lưu lại file, bạn hãy thử dùng chức năng "Save As" để lưu kết quả lên file khác trước khi thoát khỏi Word để không bị mất dữ liệu. Sau đó kiểm tra lại file Word cũ xem có gì bất thường không, bạn có thể gửi file Word cũ này cho chúng tôi khảo sát xem nguyên nhân cụ thể nào khiến Word không thể ghi lên nó được.

    Câu hỏi:
    Cài từ điển Cambridge Pronuncing ở chế độ full trên ổ cứng, duyệt thư mục cài đặt tôi thấy có các file dữ liệu có phần nới rộng là *.tda. Xin hỏi có chương trình nào có thể rút trích nội dung trong các file này để dùng cho việc riêng không?

    Trả lời:
    Các file *.tda chứa các thông tin (văn bản, hình, âm thanh,...) về các từ điển được quản lý bởi ứng dụng Cambridge English Pronouncing Dictionary. Hiện nay, chúng tôi chưa thấy công ty "Training and Development Agency" (là đơn vị định nghĩa định dạng file *.tda) công bố định dạng cụ thể của file này. Điều này cũng dễ hiểu vì họ không muốn người khác dễ dàng dùng những thông tin mà họ phải bỏ nhiều công sức để xây dựng. Như vậy, nếu muốn dùng lại dễ dàng các dữ liệu trong các file *.tda, bạn hãy thử liên hệ trực tiếp với TDA.

    Câu hỏi:
    Xin hỏi lập trình quản lý bán hàng qua mạng viết bằng jsp: Khi 1 người đang xem hàng đồng thời 1 người khác cập nhật dữ liệu, làm sao người đang xem có được ngay dữ liệu mới?

    Trả lời:
    Lưu ý, tất cả các file cấu thành website quản lý bán hàng đều được đặt ở máy server, mỗi khi người dùng yêu cầu 1 hoạt động nào, máy client sẽ gửi yêu cầu tương ứng đến server, server nhận yêu cầu, phân tích và thực hiện yêu cầu rồi gửi kết quả về để máy client hiển thị cho người dùng xem. Như vậy, nếu tại 1 thời điểm t xác định, người dùng A yêu cầu xem hàng, máy client sẽ gửi yêu cầu xem hàng đến server, server xử lý và gửi kết quả cho máy client hiển thị cho người dùng A xem. Nếu sau đó (lâu hay mau), một người dùng B khác cập nhật thông tin hàng hóa trên server, server chỉ lưu kết quả cập nhật vào database ở server chứ không có hơi sức đâu mà báo cho nhiều client biết.

    Tóm lại, việc thông báo tự động những cập nhật dữ liệu ở server cho nhiều client biết là việc rất khó thực hiện, mà nếu thực hiện được thì chạy cũng không hiệu quả, cho nên hiện nay cách mà hầu hết các server làm là nếu có yêu cầu từ client thì phục vụ ngay chứ không phục vụ nhiều lần cho client khi có sự thay đổi dữ liệu ở server. Người dùng, nếu muốn, thỉnh thoảng thực hiện lại yêu cầu để được server cho kết quả mới nhất.

    Câu hỏi:
    Máy tính mới khởi động không nghe được nhạc... Muốn nghe phải khởi động lại máy đồng thời cắm lại giắc cắm loa. Xin hỏi cách khắc phục

    Trả lời:
    Theo như bạn mô tả, máy vẫn nghe nhạc bình thường nếu được khởi động lại và cắm lại giắc cắm loa. Như vậy, nguyên nhân chính có thể nằm ở giắc cắm. Thường sau thời gian sử dụng, lỗ cắm loa bị lờn và không tiếp xúc tốt với giắc cắm, bạn nên nhờ kỹ thuật viên thay thế ổ cắm loa mới, thay vì phải tự mình tinh chỉnh mỗi lần muốn nghe loa như hiện nay.

    Câu hỏi:
    Dùng VB.NET, truy xuất database bằng các đối tượng OleDb, khi sử dụng lệnh truy vấn "Update" thì nhận thấy giá trị Update không kịp. Cụ thể tôi có 1 file database Access có tên là db1.mdb, bên trong database có 1 bảng dữ liệu tên là Table1, mỗi record của bảng Table1 có 1 field tên là Col1 thuộc kiểu số nguyên. Tôi viết 1 ứng dụng nhỏ gồm 1 form chứa 2 button: Khi click chuột vào Button2, chương trình sẽ cập nhật nội dung field Col1 (của tất cả record của bảng Table1) về giá trị 1234, còn khi click chuột vào Button1 thì chương trình sẽ cập nhật field Col1 (của tất cả record) về giá trị 4 rồi hiển thị giá trị vừa cập nhật để kiểm tra, nhưng thật bất ngờ kết quả hiển thị của field Col1 không bằng 4 mà bằng 1234. Như vậy lệnh Update chưa cập nhật kịp dữ liệu lên database. Xin hỏi nguyên nhân và cách khắc phục. Toàn bộ code VB .Net của ứng dụng như sau:

    'nhập cảng các class Oledb
    Imports System.Data.OleDb
    'đặc tả form ứng dụng
    Public Class Form1
    'định nghĩa các thuộc tính cần dùng
    Dim PathFileName As String = Application.StartupPath & "\db1.mdb"
    Dim CnnMain As OleDbConnection

    'thủ tục tạo kết nối đến database
    Sub Ketnoi_OleDb()
    CnnMain = New OleDbConnection
    CnnMain.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & PathFileName & ";Persist Security Info=False"
    CnnMain.Open()
    End Sub

    'thủ tục khởi tạo form ứng dụng
    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    'tạo kết nối đến database
    Call Ketnoi_OleDb()
    End Sub

    'thủ tục xử lý click chuột trên Button2
    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnGan.Click
    'cập nhật field Col1 về giá trị 1234
    Dim myOleDbCommand As New OleDbCommand("UPDATE Table1 SET Col1=1234", CnnMain)
    myOleDbCommand.ExecuteNonQuery()
    End Sub

    'thủ tục xử lý click chuột trên Button1
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnSuavaNhan.Click
    Dim da As OleDbDataAdapter
    Dim dt As DataTable
    'cập nhật field Col1 về giá trị 4
    Dim myOleDbCommand As New OleDbCommand("UPDATE Table1 SET Col1=4", CnnMain)
    myOleDbCommand.ExecuteNonQuery()
    'đọc lại field Col1 và hiển thị để kiểm tra
    da = New OleDbDataAdapter("SELECT Col1 FROM Table1", CnnMain.ConnectionString)
    dt = New DataTable
    da.Fill(dt)
    MsgBox(dt.Rows(0).Item("Col1"))
    'Tại sao kết quả là 1234 mà không phải 4?
    End Sub
    End Class

    Trả lời:
    Tình huống đọc nội dung field Col1 và hiển thị kết quả không theo ý muốn của bạn là do lỗi lập trình trong thủ tục xử lý click chuột Button1_Click. Thật vậy, bạn đã dùng biến CnnMain để miêu tả cầu nối đến database cần truy xuất, đã khởi tạo nó khi form bắt đầu chạy (trong hàm Form_Load). Lần đầu bạn click Button2 thì thủ tục Button2_Click chạy, nó cập nhật field Col1 của bảng Table1 thành 1234 rồi dừng ứng dụng, nội dung cập nhật được lưu lên file database. Lần chạy thứ hai, khi người dùng click Button1 thì thủ tục Button1_Click chạy, nó cập nhật field Col1 của bảng Table1 thành 4. Tuy nhiên, do cầu nối chưa được đóng nên các nội dung cập nhật vẫn còn nằm trong bộ đệm tạm (Cache) kết hợp với đối tượng CnnMain, chứ chưa được lưu thật sự lên file database (để đạt độ hiệu quả cao và khi cần dễ undo). Sau đó bạn tạo đối tượng OleDbDataAdapter để đọc bảng Table1 vào, tuy nhiên lệnh này dùng chuỗi ConnectionString để miêu tả database nên máy sẽ tạo 1 kết nối khác đến database (chứ không dùng kết nối do CnnMain quản lý), đọc dữ liệu hiện hành của bảng Table1 vào. Vì nội dung gốc của bảng Table1 trên file chưa bị cập nhật bởi lệnh Update nên khi máy hiển thị thì bạn vẫn thấy nó (1234). Bạn có thể dùng 1 trong 2 cách khắc phục lỗi trên như sau:

    - Cách 1: Thêm lệnh đóng cầu nối CnnMain sau khi đã thực hiện lệnh Update để máy ghi ngay kết quả lên file.
    Code của thủ tục Button1_Click sẽ như sau:
    'thủ tục xử lý click chuột trên Button1
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnSuavaNhan.Click
    Dim da As OleDbDataAdapter
    Dim dt As DataTable
    'cập nhật field Col1 về giá trị 4
    Dim myOleDbCommand As New OleDbCommand("UPDATE Table1 SET Col1=4", CnnMain)
    myOleDbCommand.ExecuteNonQuery()
    CnnMain.Close()
    'đọc lại field Col1 và hiển thị để kiểm tra
    da = New OleDbDataAdapter("SELECT Col1 FROM Table1", CnnMain.ConnectionString)
    dt = New DataTable
    da.Fill(dt)
    MsgBox(dt.Rows(0).Item("Col1"))
    'kết quả chắc chắn là 4.
    End Sub

    - Cách 2: Dùng tham số CnnMain thay vì dùng chuỗi ConnectionString trong lệnh tạo đối tượng OldDbdataApdater để đối tượng này dùng lại cầu nối cũ do CnnMain miêu tả, nhờ đó sẽ truy xuất được dữ liệu đã được cập nhật trước đó (mặc dù chúng chưa được ghi lên file database).
    Code của thủ tục Button1_Click sẽ như sau:
    'thủ tục xử lý click chuột trên Button1
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnSuavaNhan.Click
    Dim da As OleDbDataAdapter
    Dim dt As DataTable
    'cập nhật field Col1 về giá trị 4
    Dim myOleDbCommand As New OleDbCommand("UPDATE Table1 SET Col1=4", CnnMain)
    myOleDbCommand.ExecuteNonQuery()
    'đọc lại field Col1 và hiển thị để kiểm tra
    da = New OleDbDataAdapter("SELECT Col1 FROM Table1", CnnMain)
    dt = New DataTable
    da.Fill(dt)
    MsgBox(dt.Rows(0).Item("Col1"))
    'kết quả chắc chắn là 4.
    End Sub

    Câu hỏi:
    Tôi dùng VB 2005 lập trình với cơ sở dữ liệu Access 2003, gặp khó khăn trong việc tạo form nhập dữ liệu. Xin hỏi trong VB 2005 có Data form wizard không?

    Trả lời:
    Trong môi trường lập trình VB 2005, ta không cần đến tiện ích "Data Form Wizard" nữa vì từng phần tử giao diện trong form bất kỳ (ListBox, TextBox,...) đều có khả năng "data-binding" trực tiếp tới dữ liệu tương ứng trong database. Để demo việc tạo form để xem/hiệu chỉnh/thêm/bớt dữ liệu trực quan, chúng tôi sẽ viết 1 ứng dụng đơn giản, form ứng dụng có 1 đối tượng DataGridView và 1 đối tượng Button. Đối tượng DataGridView sẽ được dùng để hiển thị toàn bộ các record trong 1 bảng dữ liệu Access, nó cho phép người dùng xem/hiệu chỉnh/thêm/bớt dữ liệu trực quan. Đối tượng Button được dùng để giúp người dùng ra lệnh lưu các kết quả cập nhật trong DataGridView lên database tương ứng. Sau đây là qui trình điển hình để viết 1 ứng dụng này:

    1. Chạy Visual Studio 2005, chọn menu File.New Project để hiển thị cửa sổ New Project.

    2. mở rộng mục "Visual Basic" trong listbox "Project Types", chọn mục Windows rồi icon "Windows Application" trong listbox Templates, nhập tên Project là "UpdateTable", nhấn OK để máy hoàn thành việc tạo project.

    3. Khi cửa sổ thiết kế form hiển thị, vẽ lần lượt các đối tượng DataGridView và Button vào form (bằng cách chọn đối tượng tương ứng trong cửa sổ ToolBox rồi vẽ nó ở vị trí và kích thước mong muốn). Form sẽ có dạng:

    4. Tên mặc định của DataGridView là DataGridView1, tên mặc định của button là Button1. Chọn button để hiển thị cửa sổ thuộc tính của nó (thường ở dưới phải màn hình), hiệu chỉnh lại thuộc tính (Name) của nó thành btnUpdate, thuộc tính Text của nó thành "Update".

    5. Chọn button DataSource ở dưới cửa sổ Project (thường nằm trên phải màn hình), chọn lệnh "Add Project Data Source" để hiển thị cửa sổ "Data Source Config...".

    6. Chọn icon DataBase, click button Next, nhấn New Connection để hiển thị cửa sổ "Add Connection".

    7. Chọn mục "Microsoft Access Database File (OLE DB)" cho textbox DataSource, nhấn Browse để duyệt tìm và chọn file database chứa bảng dữ liệu cần truy xuất. Nhấn "Test Connection" để kiểm tra xem có truy xuất được không, nếu được thì nhấn Ok để đóng cửa sổ hiện hành lại.

    8. Đóng lần lượt các cửa sổ tới khi gặp cửa sổ chứa textbox "DataSet Name", hiệu chỉnh tên cho DataSet là MyDataSet. Duyệt trong danh sách các bảng của database, chọn bảng dữ liệu cần xử lý (giả sử file database của bạn có bảng dữ liệu tên Danhbadienthoai), nhấn Finish để hoàn thành việc định nghĩa data source cần dùng.

    9. Chọn đối tượng DataGridView để hiển thị cửa sổ thuộc tính của nó, duyệt tìm thuộc tính Data Source, nhấn chuột vào dấu “V” bên phải mục này rồi duyệt tìm và chọn bảng Danhbadienthoai. Đây là nguồn dữ liệu mà đối tượng DataGridView sẽ hiển thị cho người dùng làm việc với nó. Bây giờ bạn sẽ thấy ngay dưới form thiết kế có 3 phần tử mới được tạo ra: MyDataSet, DanhbadienthoaiBindingSource, DanhbadienthoaiTableAdapter. Qui trình tương tác dữ liệu giữa các phần tử như sau: DataGridView n DanhbadienthoaiBindingSource n MyDataSet n DanhbadienthoaiTableAdapter n file Database *.mdb của bạn. Khi ứng dụng chạy, mỗi khi người dùng hiệu chỉnh/thêm/bớt record trực tiếp trên DataGridView, nội dung được hiệu chỉnh/thêm/bớt chỉ được chứa cục bộ trong đối tượng DataSet tương ứng chứ chưa được lưu ngay lên database. Muốn lưu thông tin hiện có trong DataSet lên Database, chương trình cần phải thực hiện tường minh lệnh Update của đối tượng TableAdapter.

    10. Nhấn đúp chuột vào button Update để tạo hàm xử lý sự kiện click chuột cho nó rồi viết code sau vào thân hàm xử lý này:
    Private Sub btnUpdate_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnUpdate.Click
    Me.DanhbadienthoaiTableAdapter. Update(Me.MyDataSet.Danhbadienthoai)
    End Sub

    11. Việc xây dựng ứng dụng hiện đã hoàn tất, hãy chọn menu Debug.Start Debugging để thử dịch và chạy thử ứng dụng. Nếu không có lỗi thì ứng dụng sẽ chạy và hiển thị ngay nội dung trong bảng Danhbadienthoai lên DataGridView, người dùng có thể duyệt xem/xóa/sửa/thêm mới record theo yêu cầu. Khi nào cần lưu lên database, hãy nhấn button Update.

    ID: A1102_98