0

I'm actually creating a method to get details for my asp Chart based on a certain datetime range. I've looked at these 2 links,

First Link , Second Link

My method to retrieve data,

//Monthly Statistics
public int janLogin(String username)
{
    int result = 0;

    StringBuilder sqlCmd = new StringBuilder();
    sqlCmd.AppendLine("SELECT COUNT(*) FROM AuditActivity WHERE Username = @getUsername AND DateTimeActivity BETWEEN @getFirstDT AND @getLastDT AND ActivityType = @getType");

    try
    {
        SqlConnection myConn = new SqlConnection(DBConnectionStr);

        myConn.Open();

        SqlCommand cmd = new SqlCommand(sqlCmd.ToString(), myConn);

        //DateTime
        DateTime currentDT = DateTime.Today;

        //Code Below gets only current Start and End of Current month
        DateTime FirstDT = currentDT.AddDays(1 - currentDT.Day);
        DateTime SecondDT = FirstDT.AddMonths(1).AddSeconds(-1);

        cmd.Parameters.AddWithValue("@getUsername", username);
        cmd.Parameters.AddWithValue("@getFirstDT", FirstDT);
        cmd.Parameters.AddWithValue("@getLastDT", SecondDT);
        cmd.Parameters.AddWithValue("@getType", "Login");

        result = Convert.ToInt16(cmd.ExecuteScalar());

        myConn.Close();

        return result;
    }
    catch (SqlException ex)
    {
        logManager log = new logManager();
        log.addLog("AuditNLoggingDAO.janLogin", sqlCmd.ToString(), ex);
        return 0;
    }
}

What i'm trying to actually do as you can see from the method name janLogin basically to get how many people logged in from 1 Jan 2017 00:00:00 to 31 JAN 2017. Current month is actually August. How can i actually get for all twelve months (Jan to Dec) the start and end month (from the bold example) for current year of 2017?

If i'm not wrong it's having to involve a for loop? Where the end count is 12. But i'm quite sure on how to do it.. There might be a duplicated question but i cant seem to find one that fits my requirement..

Appreciate any help. Thank you!

domster
  • 556
  • 2
  • 8
  • 26
  • 1
    Start not using AddWithValue in particular with Dates [Can we Stop using addWithValue already?](https://blogs.msmvps.com/jcoehoorn/blog/2014/05/12/can-we-stop-using-addwithvalue-already/) – Steve Aug 05 '17 at 06:47
  • Second can you explain what is the datatype of the two columns in your database? If they store also the time part your code can be bugged – Steve Aug 05 '17 at 06:49
  • @Steve thanks for the suggestion and the article, will amend in the future! DateTime is the datatype for firstDT and lastD – domster Aug 05 '17 at 06:59

4 Answers4

2

The entire 2017 calendar year

The whole calendar year:

int year = 2017
DateTime FirstDT = new DateTime(year, 1, 1); // jan 2017
DateTime SecondDT = FirstDT.AddYear(1).AddSeconds(-1); // last second of dec 2017

The example above will return 01 Jan 2017 00:00:00 to 31 Dec 2017 23:59:59
Note the issue with precision at the bottom of this answer for 31 DEC 2017 23:59:59.999999


A particular month in a year

In order to get a different month, instead of using the current one, you can do it like this:

int year = 2017;
int month = 12;
DateTime FirstDT = new DateTime(year, month , 1); // december 2017
DateTime SecondDT = FirstDT.AddMonths(1).AddSeconds(-1); // last second of dec 2017

The example above will return 01 Dec 2017 00:00:00 to 31 Dec 2017 23:59:59
Note the issue with precision at the bottom of this answer for 31 DEC 2017 23:59:59.999999

Then you can alter your method to take the month as a parameter like so:

public int LoginsByMonth(int year, int month, String username)
{
    if (month < 1 || month > 12)
    {
        throw new ArgumentOutOfRangeException("month must be between 1 and 12.");
    }

    ...
    
    DateTime FirstDT = new DateTime(year, month, 1);
    DateTime SecondDT = FirstDT.AddMonths(1).AddSeconds(-1);

    ...
}

and you can call your method like this as singles:

 int loginsNovember2017 = LoginsByMonth(2017, 11, "userA");
 int loginsDecember2017 = LoginsByMonth(2017, 12, "userA");

Precision: Using Ticks instead of Seconds

As noted in the comments below. You could use .AddTicks(-1L) instead of .AddSeconds(-1) to get a more accurate filter. However, depending on your requirements you may prefer to go with avoiding this altogether with the next option.

Avoiding precision

Also as noted in the comments below. You could avoid this precision headache alltogether by switching up your SELECT stament to include a < (less than) condition in your WHERE statement instead of worrying about the precision when using the BETWEEN condition.

The changes would look like this

SELECT COUNT(*)
FROM AuditActivity
WHERE Username = @getUsername 
    AND DateTimeActivity >= @getFirstDT
    AND DateTimeActivity < @getLastDT
    AND ActivityType = @getType

Which means you can drop the AddSeconds(-1) or AddTicks(-1L). For example:

// entire year
DateTime SecondDT = FirstDT.AddYear(1);
// month
DateTime SecondDT = FirstDT.AddMonths(1);
Community
  • 1
  • 1
Svek
  • 12,350
  • 6
  • 38
  • 69
  • 1
    Doing `.AddMonths(1).AddSeconds(-1)` is a bit poor. You really should do `.AddMonths(1).AddTicks(-1L)` to get the last possible representation of a time before the new month starts. – Enigmativity Aug 05 '17 at 06:52
  • 1
    @Enigmativity -- The OP asked specifically `1 Jan 2017 00:00:00` (seconds) in his question. But in your case, you are correct, `Ticks` is the correct way. I'll put a note on the answer. – Svek Aug 05 '17 at 06:53
  • Sorry, you're right. I should really have put the comment on the question. – Enigmativity Aug 05 '17 at 07:01
  • I seen AddTicks in the link that i shared in my question, what exactly is it for? For precision of AddSeconds(-1)? Edit: I just saw that yup it's for precision. – domster Aug 05 '17 at 07:01
  • 1
    @domster - Yes, it's possible that things can happen in the last second of the day. Buy doing `.AddSeconds(-1)` you will miss those. – Enigmativity Aug 05 '17 at 07:03
  • 1
    @domster - It will include fraction of seconds between the 59 sec and 00 sec to the highest degree of precision that can be found using the `DateTime` struct – Svek Aug 05 '17 at 07:03
  • I have 2 queries.. for the method under 'A particular month in a year' it's actually hardcoding the month (1-12)? And for addTicks(param) what should be the param be? Or what does it represent? – domster Aug 05 '17 at 07:05
  • @Svek `Ticks` is too small for `DateTime` in SQL Server. – Akash Kava Aug 05 '17 at 07:08
  • @domster `if (month < 1 || month > 12)` is making sure you don't try and pass a value into month like `-1`, `0` or `13`. – Svek Aug 05 '17 at 07:08
  • @Svek oh okay.. Yup i get that part but for the december 2017 is to actually hardcode 12 as the month in new DateTime(x,x,x)? – domster Aug 05 '17 at 07:10
  • 1
    @domster The `DateTime` constructor we are using is `new DateTime(year, month, day)`. So, passing `month = 12` will give you December. – Svek Aug 05 '17 at 07:15
  • @Svek got it! Last query out of curiosity.. Do the codes know about leap years/number of days for each month? – domster Aug 05 '17 at 07:16
  • @domster - yes. (.NET does) but I can't guarantee all databases that you query will. – Svek Aug 05 '17 at 07:22
  • why do you have to bother yourself (and the next programmer who will have to touch this code) with all this precision stuff? why wouldn't you use the strict less operator God gave you (`loginDate < firstDayOfNextMonth`)? or just compare the date part of your `DateTime` (`loginDate.Date <= lastDayOfMonth`) – vorou Aug 05 '17 at 07:57
  • @vorou - From what I understand of the question, the objective is to pass an SQL query which will return the desired result. I'm not exactly sure how your approach produces a workable parameter for the SQL query. – Svek Aug 05 '17 at 08:29
  • @Svek yep, you're right, totally missed that SQL while reading the question. Still, as pointed out by [this answer](https://stackoverflow.com/a/749663/1085375), it's better to avoid using BETWEEN in this case, and rewrite the query to use `firstDayOfThisMonth < x.Date AND x.Date < firstDayOfNextMonth` – vorou Aug 05 '17 at 09:32
  • @vorou - it's a matter of perspective. There isn't a "correct" way between choosing `BETWEEN` or a duo condition statement with your proposed `AND` statement. There are gotcha's in either case. -- an uninformed coder would equally mistake `today < Date AND Date <= tommorrow` as it should include tomorrow, but in fact `tomorrow` is a repsentation of exactly midnight. At the end of the day, it's just preference. – Svek Aug 05 '17 at 09:43
  • @Svek @domster I guess it's not about personal preference, if the column type is `datetime` you just can't make it right with `BETWEEN`. Please see the second part of [my answer](https://stackoverflow.com/a/45519113/1085375) for the details. – vorou Aug 05 '17 at 10:25
  • @vorou -- The column type is unspecified in this question. SQL can be a big range of technologies where precision is determined by the underlying database. We are not discussing the C# `DateTime` struct -- and you keep coming back to it. – Svek Aug 05 '17 at 10:29
  • @Svek I guess we are, since we are using it to build the query for SQL. SQL being a big range of technologies with different precisions is exactly why it seems better to me to use the solution which does not depends on the specific precision. – vorou Aug 05 '17 at 10:35
  • @vorou - for completeness I'm adding it, but please note -- as I stated in my first comment, the OP specifically asked for seconds when the post was originally asked. – Svek Aug 05 '17 at 10:46
1

So, I guess there are two different questions here.

The first one is "How to get a date range for a specific month by year and month?"

Here's a way:

var year = 2017;
for (int month = 1; month <= 12; month++)
{
    var firstDayOfMonth = new DateTime(year, month, 1);
    var lastDayOfMonth = new DateTime(year, month, DateTime.DaysInMonth(year, month));
    Console.WriteLine($"{firstDayOfMonth} - {lastDayOfMonth}");
}

Keep in mind that there is no Date-only values of DateTime. If you omit the time part in the constructor like I did, it will be set to the midnight of that day. When using these values to filter some dates, you should either call .Date on it first:

var datesInThisMonth = someDates.Where(x => x >= firstDayOfMonth && x.Date <= lastDayOfMonth);

...or use strict less with the first day of next month as an upper bound:

var datesInThisMonth = someDates.Where(x => x >= firstDayOfMonth && x <= lastDayOfMonth.AddDays(1));

And the second one is how to use that value to build an SQL query.

You should not use BETWEEN here. Depending on what data type you use for the column it's either hard to digest or just plain wrong.

Here's the alternative you have:

  • Your column is of datetime type

Then it's rounded to .000, .003, .007 seconds. So, the upper bound you will use in your BETWEEN statement will be rounded to either 23:59:59.997 or 00:00 of the next day. You will either miss some records, or include some records multiple times (it will be both in the results for this month and for the next one).

  • Your column is of datetime2 type

Then it could be possible to write the correct query, but again in order to do it right you'll have to consider what precision do you use in SQL (it could be customized for that specific column of this specific table) and what value should you pass from C# to get the right value in SQL.

I don't see how it's worth the hassle.

Just use x.Date > firstDayOfMonth AND x.Date < firstDayOfNextMonth and forget about it.

You can also take a look at this answer.

vorou
  • 1,869
  • 3
  • 18
  • 35
  • hey vorou i'll try it out and see what i get with this, thanks! – domster Aug 05 '17 at 06:58
  • This would miss out on all logins for the last day of each month. – Enigmativity Aug 05 '17 at 07:05
  • @Enigmativity it depends on how you use this code to filter the logins; I would write `.Where(x => x.LoginDate.Date <= lastDayOfMonth)` and would not miss anything – vorou Aug 05 '17 at 07:53
  • @vorou - But it would miss out like that. The variable `lastDayOfMonth` as you've defined it is midnight on the last day of the month. Since midnight is the beginning of the day you're missing out on the final 24 hours of the month - i.e. any time that occurs during that last day. – Enigmativity Aug 05 '17 at 08:37
  • @Enigmativity please re-read my comment: I'm using `.Date` property of `DateTime`. It returns midnight as well, so for any `DateTime` on the last day of the month, it would be equal to what I have in `lastDayOfMonth`. – vorou Aug 05 '17 at 09:22
  • @vorou - Sorry, I missed that. It'd probably be worth noting that in your answer. That kind of usage isn't very obvious. – Enigmativity Aug 05 '17 at 09:31
  • @Enigmativity right, edited my answer to make it clear. Thanks for pointing out. – vorou Aug 05 '17 at 09:41
1

Why don't you just use the function that SQL gives you?

@"SELECT MONTH(DateTimeActivity), COUNT(*) FROM AuditActivity 
  WHERE Username = @getUsername AND ActivityType = @getType
  GROUP BY MONTH(DateTimeActivity)
  ORDER BY MONTH(DateTimeActivity)"

This will give you 12 records with count for each month (assuming you have dates for all year's months) with just ONE single call to the database and not 12 different calls inside a loop. And the WHERE statement is a lot simpler with no calculations on the DateTimeActivity field

This approach has just one problem. It doesn't return a record with zero value if the WHERE statement doesn't find any record with the given constraints.

But in code is relatively easy to handle the eventual missing months

    cmd.Parameters.AddWithValue("@getUsername", username);
    cmd.Parameters.AddWithValue("@getType", "Login");

    SqlDataReader reader = cmd.ExecuteReader();
    int[] monthCount = new int[12];
    while(reader.Read())
        // -1 because arrays start at zero and January has value 1.
        monthCount[reader.GetInt32(0) - 1] = reader.GetInt32(1);

At the end you have an array of integers where each element represents the count of each month with zero for months with no records after the givens constraints in the WHERE statement are applied.

Steve
  • 213,761
  • 22
  • 232
  • 286
  • hey Steve, it returns all 12 months? – domster Aug 05 '17 at 07:11
  • If you have at least one DateTimeActivity for each month, otherwise there will be no record for that month. It can be easily handled in code though. There are also alternative to get a zero with the month directly from sql server but I have not an example at hand now. – Steve Aug 05 '17 at 07:12
  • i tried placing the statement into sql query but gives back no rows at all.. I know it should be just one row. with the Month, and the count. I used this: SELECT MONTH(DateTimeActivity) AS 'DateTimeActivity', COUNT(*) AS 'CountTimes' FROM AuditActivity WHERE Username = 'x@gmail.com' AND ActivityType = 'Login Failuire' GROUP BY MONTH(DateTimeActivity) ORDER BY MONTH(DateTimeActivity) – domster Aug 05 '17 at 07:42
  • LoginFailuire ? – Steve Aug 05 '17 at 07:44
  • A part from the possible typo. I have tested it against a table of mine directly with SQL Server and it gives back all the records present grouped by month. I can't tell exactly what is the problem. Of course the DateTimeActivity is a DateTime column right? – Steve Aug 05 '17 at 07:49
  • yup it is a datetime column in my table – domster Aug 05 '17 at 08:13
1

This should get the logins for the current year as an array of integers representing each month):

public int[] yearLogin(String username)
{
    string sqlCmd = @"SELECT COUNT(*)
FROM AuditActivity
WHERE Username = @getUsername AND DateTimeActivity
BETWEEN @getFirstDT AND @getLastDT AND ActivityType = @getType";

    try
    {
        Func<SqlConnection, DateTime, DateTime, int> fetch = (c, f, t) =>
        {
            using (SqlCommand cmd = new SqlCommand(sqlCmd.ToString(), c))
            {
                cmd.Parameters.AddWithValue("@getUsername", username);
                cmd.Parameters.AddWithValue("@getFirstDT", f);
                cmd.Parameters.AddWithValue("@getLastDT", t);
                cmd.Parameters.AddWithValue("@getType", "Login");

                return Convert.ToInt16(cmd.ExecuteScalar());
            }
        };
        using (SqlConnection myConn = new SqlConnection(DBConnectionStr))
        {
            myConn.Open();

            DateTime currentDT = DateTime.Today;
            DateTime FirstDT = currentDT.AddMonths(1 - currentDT.Month).AddDays(1 - currentDT.Day);

            int[] result =
                Enumerable
                    .Range(0, 12)
                    .Select(x => fetch(myConn, FirstDT.AddMonths(x), FirstDT.AddMonths(x + 1).AddTicks(-1L)))
                    .ToArray();

            return result;
        }
    }
    catch (SqlException ex)
    {
        logManager log = new logManager();
        log.addLog("AuditNLoggingDAO.janLogin", sqlCmd.ToString(), ex);
        return null;
    }
}
Enigmativity
  • 113,464
  • 11
  • 89
  • 172