Decimal values being rounded in Sqlite

Asked

Viewed 1,403 times

3

After migrating my database from Access to Sqlite 3 in a Windows Forms application, I have been having difficulties with Decimal values. If I run a simple select:

SELECT * FROM Item WHERE id = 1;

The decimal values of the preco_item column are correctly displayed, with 2 decimal places. Example: 2.53 = 2.53. But when I do a query a little more complex, with related tables as following:

SELECT Produto.nome AS Produto, 
                Item.quantidade AS Quantidade, 
                Item.preco_item AS Preço,     
                Item.preco_item * Item.quantidade as Subtotal,               
                Item.id  
            FROM Item  
            INNER JOIN Orcamento ON Item.id_orcam = Orcamento.id   
            INNER JOIN Produto ON Item.id_produto = Produto.id   
            WHERE(Orcamento.id = 1 );

The results obtained in the Price line are presented in DataTable thus: 2,53 = 2,00, thus also happens with the result of multiplication preco_item * quantidade.

Note: Monetary items are set to Decimal(7.2) in Sqlite. I have tried several other formats. In the query by Sqlite Studio, the price results present normally, with the exception of multiplication that I have not yet found a way to calculate correctly. Always returns an integer value to me.

P.S.: Values are entered into a Datagridview via Datatable.

Update to demonstrate BD schema:

private static string createQuery()
{
        string createScript = "CREATE TABLE Cliente (" +
            " id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
            " nome       TEXT NOT NULL UNIQUE," +
            " endereco   TEXT," +
            " local      TEXT," +
            " telefone_1 TEXT," +
            " telefone_2 TEXT ); ";
        createScript += "CREATE TABLE Produto (" +
             " id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
             " nome  TEXT NOT NULL UNIQUE," +
             " preco DECIMAL ); ";
        createScript += "CREATE TABLE Tipo (" + 
            " id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"  + 
            " nome      TEXT," + 
            " descricao TEXT ); ";
        createScript += "CREATE TABLE Orcamento (" +
            " id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
            " id_cliente INTEGER REFERENCES Cliente (id)," +
            " data DATETIME," +
            " id_tipo INTEGER REFERENCES Tipo(id)," +
            " desconto DECIMAL," +
            " status TEXT DEFAULT Pendente ); ";
        createScript += "CREATE TABLE Item (" +
            " id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
            " id_orcam INTEGER REFERENCES Orcamento (id)," + 
            " id_produto INTEGER REFERENCES Produto(id)," +
            " preco_item DECIMAL," + 
            " quantidade INTEGER NOT NULL ); ";

        return createScript;
}

Here is the code that fills the Datagridview:

string query = Queries.GetItemsByOrcId(orcId);
sql.CustomSelect(query);
dgvpProd.DataSource = sql.Datatable;
  • 3

    There is no "Decimal(7,2)" in Sqlite. Either it is INTEGER, or it is REAL. And REAL you should not use under any circumstances for price. The least painful path is usually to create an INTEGER of the name preco_item100, for example, and save 2.35 as 235. Or preco_item1000 if you need 3 houses, as the gas station staff does. (this number in the name is a technique to make life easier and you always know where to put the comma and/or how much is the divider, it’s just a hint). In the worst case it has to store as character, but then I understand it causes other problems.

  • 1

    Note: to tell the truth, even when the bank supports, I still find the price 100 better than decimal, unless you need some abstraction that the type offers on the client language side.

  • I get it. I’ll try removing the commas in Insert and find a way to treat the values as a currency format in c#. Thank you.

  • Hello friend, unfortunately I’m already refactoring everything according to Bacco’s tip. But in short I was generating a datatable with the result of select and using the Datagridview Source method to populate it. The price column type was decimal according to the BD column model, but the result always rounded when it was displayed in the DGV. Now I put the BD columns as int, dividing by 100 and parsing the decimal result for string and using the columns with type string.

  • Ali has the second select mentioned in the post.

  • 1

    @Vanderlei here has several explanations as to why one should not use floating point with money. This is a basic premise of any trusted system: http://answall.com/questions/5746/bestdatatypes-workingwith money

  • 2

    @Vanderlei notes that in principle, in the Sqlite affinity manual the Decimal is converted to "number", which is stored as "integer" if it has no decimal places and "float" when it has one. The fact that it works with "real", however, does not mean that it should be used, precisely because of the cumulative rounding problems in any mathematical operations. (often you store a 23.50, but the REAL stores as 23.499999999.... - on the screen appears right, but when doing a multiplication by quantity, starts to give error, even on the screen appearing . 50)

  • 2

    And this is not a problem unique to the language A or B, but the way the floating numbers work. One more example, from the outside: https://javeiros.wordpress.com/2008/06/25/evite-float-e-double-se-voce-quer-respostas-exact/ - if you search for "float" and "money" you will find a lot about the subject. Dbs that have "decimal" and "money" type do not use float internally, but a fixed format. As Sqlite does not have it native, you need to adapt some kind of it. Only the integers and the texts are left. The integer is more efficient and saves space, the text is simpler to view.

  • 1

    In fact wear real in Sqlite for monetary value does not give. To see about Sqlite: http://answall.com/q/78306/101. To see about the problem in general: http://answall.com/q/38138/101. This is something basic that every programmer who already has a minimum of experience ends up learning. It is a problem related to the processor and all languages and databases suffer from it. The detailed explanation is in these questions, but there are several other answers from various authors on and off the site. There are links to other sources for those who have not yet learned about the subject.

  • 1

    I am refactoring according to your experience. I should take some time, since it is a project as a hobby. I will store as int, divide by 100 and present in DGV as string. This would be the way?

  • 1

    @Vanderlei the important thing is to understand where the float "betrays" our expectations (the fact that you see a 3.50, but the PC see 3.4999999982337). The whole has too much work to divide, but if you drop the rounding to the last step, it ensures that the inaccuracy will not propagate by multiple calculations.

  • 1

    Regardless of storage, another example of care when making monetary calculations, is to always maintain the chosen precision, not leaving "broken" more. If for example to sell a product fractionated whose calculation value x quantity gave 23,731 cents, and on the screen appears 23.73 only, you need to discard this 1 more at the end before adding up the total (add 23.73, not 23.731), but this "subcentavo" invisible will join with other broken subtotal, and in the end the customer has an extra penny sum without knowing where it came from, burning reliability and accounting.

Show 7 more comments

1 answer

2

I have found a solution based on the suggestions of colleagues. The monetary value fields were created as INTEGER and I treated the entire decimal formatting via code in the application. The class structure that I created allowed me to convert the values into Datatable before arriving at the application dataGridview. Follows the main method of the class:

public static DataTable dtColumnIntToDecimal(DataTable dt, int column)
    {
        List<decimal> price = new List<decimal>();
        //Gerar lista de valores em decimal
        foreach (DataRow row in dt.Rows)
        {                
            decimal aux = Convert.ToDecimal((long)row[column]) / 100;
            price.Add(aux);     
        }

        //clonar o datatable, transformando a coluna em decimal
        DataTable dtCloned = dt.Clone();
        dtCloned.Columns[column].DataType = typeof(decimal);
        foreach (DataRow row in dt.Rows)
        {
            dtCloned.ImportRow(row);
        }

        //Inserir valores em decimal
        for (int rowIndex = 0; rowIndex < dt.Rows.Count; rowIndex++)
        {
            dtCloned.Rows[rowIndex][column] = price[rowIndex];
            Console.WriteLine(dtCloned.Rows[rowIndex][column]);
        }

        //atualiza o total de valores (opcional) 
        valorTotal = price.Sum();
        //Retorna o DataTable
        return dtCloned;
    }

Code for use of class:

//Minha consulta
sql.Select("Produto", tbConsulta.Text);                        
//DataTable retornado
DataTable dt = Util.dtColumnIntToDecimal(sql.Datatable, 2);
//Aplicação do DataTable ao DataGridView
dataGridView1.DataSource = dt;

... and for the currency format to appear correctly without deleting the zeroes after the comma, just apply the "C2" in Defaultcellstyle.Format in the column.

  • 1

    +1, legal. Another challenge will be to store dates :) But I already tell you that there are 2 very common formats for this: integer, if you want Unix time, which is counting in seconds since 1/1/1970, and real if you use Pr. Julian Day, which is the number of days since 11/24/-4714. In the second case, the hours are represented in float, being 0.5 equivalent to noon. Sqlite has functions to work with both situations, it is the case to see which combines best with the client language and the application as a whole. I like Unix time better for being whole, but it doesn’t have thousandths of a second. It goes from need.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.