Quite a beginner question: given a DateTime, I want to find the previous non-weekend date in F#. I thus created this:
open System
module Date =
let isWeekEnd (date:DateTime) =
((date.DayOfWeek = DayOfWeek.Saturday) || (date.DayOfWeek = DayOfWeek.Sunday))
let getPreviousWorkDay date =
while isWeekEnd date do
let date = date.AddDays -1.0
date
However, it seems I violate the fact that date is immutable. I hence wonder how to do this with a while loop and why not with a sequence by retrieving up to 3 days back and filtering the one which is nonweekend, finally taking the first...
If you want to use that while loop approach you need to declare date as mutable:
let getPreviousWorkDay d =
let mutable date = d
while isWeekEnd date do
date <- date.AddDays -1.0
date
Otherwise you can use recursion:
let rec getPreviousWorkDay date =
let prevDate = date.AddDays -1.0
if isWeekEnd prevDate then getPreviousWorkDay prevDate
else prevDate
Note that if what you want is really to go one day before and then if it's a weekend go further back, your function will stay in case of sending a weekday. But you can solve it by passing an already subtracted day:
let getPreviousWorkDay (date:DateTime) =
let rec loop date =
if isWeekEnd date then loop (date.AddDays -1.0)
else date
loop (date.AddDays -1.0)
The very nature of using a while loop means that you're going to be mutating something. If you don't want to do this and would rather go with a more functional solution then you could use a match expression:
module Date =
let getPreviousWorkDay (date:DateTime) =
match date.DayOfWeek with
| DayOfWeek.Monday -> date.AddDays -3.0
| DayOfWeek.Sunday -> date.AddDays -2.0
| _ -> date.AddDays -1.0
Here we don't need to mutate anything.
Related
I am trying to write a code where I have got two time[hh:min] data(String type). Need to just compare but the challenge is my code undergones some validations before returning the final values. so the assertion fails sometimes stating expected value is [17:04] but actual is [17:05]. Is there any way where we can use concept of Threshold that upto few minutes (say 2 mins) the comparison will still be valid?
Step one is do not store a thing as something that it is not. If these are times, they should be stored as times. Strings are for representation to the users; underlying storage is for reality.
So now let's store our times as date components:
let t1 = DateComponents(hour:17, minute:4)
let t2 = DateComponents(hour:17, minute:5)
Now it's easy to find out how far apart they are:
let cal = Calendar(identifier: .gregorian)
if let d1 = cal.date(from: t1),
let d2 = cal.date(from: t2) {
let diff = abs(d1.timeIntervalSince(d2))
// and now decide what to do
}
You first need to seprate your string to an array, and then you can compare.
/* That two arrays are A1 and A2 */
let minute1 = Int(A1[0])*60+Int(A1[1])
let minute2 = Int(A2[0])*60+Int(A2[1])
This may help you. I think that #Sweeper did not understand that it is a time, not a date.
You can convert your string to minutes, subtract one from another and check if the absolute value is less than the threshold:
extension String {
var time24hToMinutes: Int? {
guard count == 5, let hours = Int(prefix(2)), let minutes = Int(suffix(2)), Array(self)[2] == ":" else { return nil }
return hours * 60 + minutes
}
func time24hCompare(to other: String, threshold: Int = 2) -> Bool {
guard let lhs = time24hToMinutes, let rhs = other.time24hToMinutes else { return false }
return abs(lhs-rhs) < threshold
}
}
Testing:
"17:02".time24hCompare(to: "17:04") // false
"17:03".time24hCompare(to: "17:04") // true
"17:04".time24hCompare(to: "17:04") // true
"17:05".time24hCompare(to: "17:04") // true
"17:06".time24hCompare(to: "17:04") // false
I would like to find out the index of an array by an NSDate.
Ill tried:
var sectionsInTable = [NSDate]()
let indexOfElement = sectionsInTable.indexOf(date) // where date is an NSDate in my sectionsInTable Array
print(indexOfElement)
But ill always get false
How is it possible to get the index of an NSDate from an array?
Thanks in advance.
If you have exact copies of NSDate objects, your code should work:
let date = NSDate()
let date2 = date.copy() as! NSDate
var sectionsInTable: [NSDate] = [date]
let indexOfElement = sectionsInTable.indexOf(date2)
print(indexOfElement)
//prints: Optional(0)
Your approach should work fine. This code produces an index of 2:
let s = 31536000.0 // Seconds per Year
var tbl = [NSDate]()
tbl.append(NSDate(timeIntervalSince1970: 40*s)) // 0
tbl.append(NSDate(timeIntervalSince1970: 41*s)) // 1
tbl.append(NSDate(timeIntervalSince1970: 42*s)) // 2
tbl.append(NSDate(timeIntervalSince1970: 43*s)) // 3
let date = NSDate(timeIntervalSince1970: 42*s)
let indexOfElement = tbl.indexOf(date)
The most likely reason that you are not getting the proper index is that your search NSDate has a time component that does not match the time component in NSDate objects in the array. You can confirm that this is the case by printing both objects, and verifying their time component.
Since comparison depends on how deep you want to go with date time thing. I think you should just loop through your date array and compare if it's equal and return that index.
I have an Int, say 15344 and I'm sending it to a label as a string, but I want it to be formatted as 01:53.44
var split = data.valueForKeyPath("time") as! Int
cell.textLabel?.text = split.description
but this only gives me the 15344 without format. I tried .stringbyAppendingformat but couldn't get it right. thanks in advance!
That is a strange way to store a time and perhaps you should think
about using the number of milliseconds instead. If that is not an option,
you can "dissect" the integer with
let time = 15344
let minutes = time / 10000
let seconds = (time / 100) % 100
let centis = time % 100
and then create a string with
let text = String(format:"%d:%02d.%02d", minutes, seconds, centis)
print(text) // 1:53.44
The simple solution would be
First convert it to string then,
let time = "15344"
let rangeOfSecond = Range(start: (advance(time.endIndex, -2)),
end: time.endIndex)
let secondString = time.substringWithRange(rangeOfSecond)
For minute
let rangeOfMinute = Range(start: (advance(time.endIndex, -4)),
end: time.endIndex - 2)
let minuteString = time.substringWithRange(rangeOfMinute)
Similarly for hour. Then concat all substrings with ":".
This seems like a weird way to be doing time formatting. There is an NSDate api that uses NSDateFormatter for this purpose. The documentation can be found here:
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSDateFormatter_Class/
You might want to use:
func stringFromDate(_ date: NSDate) -> String
I read on stackoverflow that easiest way to convert a DateTime variable back to Excel date was simply to do:
let exceldate = int(DateTime)
Admittedly this was in c# and not f#. This is supposed to work as the decimals represent time and int part represents date. I tried this and f# comes back with the error:
The type 'DateTime' does not support a conversion to the type 'int'
So how do I convert back to excel date?
More specificly, I m trying to create a vector of month 1st for a period between start date and end date. Both vector output and start date and end date are floats, i.e. excel dates. Here my clumsy first attempt:
let monthlies (std:float) (edd:float) =
let stddt = System.DateTime.FromOADate std
let edddt = System.DateTime.FromOADate edd
let vecstart = new DateTime(stddt.Year, stddt.Month, 1)
let vecend = new DateTime(edddt.Year, edddt.Month, 1)
let nrmonths = 12 * (edddt.Year-stddt.Year) + edddt.Month - stddt.Month + 1
let scaler = 1.0 - (float(stddt.Day) - 1.0) / float(DateTime.DaysInMonth(stddt.Year , stddt.Month))
let dtsvec:float[] = Array.zeroCreate nrmonths
dtsvec.[0] <- float(vecstart)
for i=1 to (nrmonths-1) do
let temp = System.DateTime.FromOADate dtsvec.[i-1]
let temp2 = temp.AddMonths 1
dtsvec.[i] = float temp2
dtsvec
This doesnt work because of the conversion issue and is rather complicated and imperative.
How do I do the conversion? How can I do this more functionally? Thanks
Once you have the DateTime object, just call ToOADate, like so:
let today = System.DateTime.Now
let excelDate = today.ToOADate()
So your example would end up like so:
let monthlies (std:float) (edd:float) =
let stddt = System.DateTime.FromOADate std
let edddt = System.DateTime.FromOADate edd
let vecstart = new System.DateTime(stddt.Year, stddt.Month, 1)
let vecend = new System.DateTime(edddt.Year, edddt.Month, 1)
let nrmonths = 12 * (edddt.Year-stddt.Year) + edddt.Month - stddt.Month + 1
let scaler = 1.0 - (float(stddt.Day) - 1.0) / float(System.DateTime.DaysInMonth(stddt.Year , stddt.Month))
let dtsvec:float[] = Array.zeroCreate nrmonths
dtsvec.[0] <- vecstart.ToOADate()
for i=1 to (nrmonths-1) do
let temp = System.DateTime.FromOADate dtsvec.[i-1]
let temp2 = temp.AddMonths 1
dtsvec.[i] = temp2.ToOADate()
dtsvec
In regards to getting rid of the loop, maybe something like this?
type Vector(x: float, y : float) =
member this.x = x
member this.y = y
member this.xDate = System.DateTime.FromOADate(this.x)
member this.yDate = System.DateTime.FromOADate(this.y)
member this.differenceDuration = this.yDate - this.xDate
member this.difference = System.DateTime.Parse(this.differenceDuration.ToString()).ToOADate
type Program() =
let vector = new Vector(34.0,23.0)
let difference = vector.difference
I'm attempting to write a function that generates a list of DateTimes using the generator syntax:
let dateRange =
let endDate = System.DateTime.Parse("6/1/2010")
let startDate = System.DateTime.Parse("3/1/2010")
seq {
for date in startDate..endDate do
if MyDateClass.IsBusinessDay(date) then yield date
}
but the generator ('seq') block does not parse correctly. It wants a timespan. While the generator syntax seems perfect for what I want to do, it's rather non-intuitive for anything but two numbers.
Is it possible to use the generator syntax to create a DateTime range?
is there a better way to think about how to create the range than I wrote (i.e. the 'in' clause)
If TimeSpan had a static Zero property, then you could do something like startDate .. TimeSpan(1,0,0,0) .. endDate. Even though it doesn't, you can create a wrapper that will do the same thing:
open System
type TimeSpanWrapper = { timeSpan : TimeSpan } with
static member (+)(d:DateTime, tw) = d + tw.timeSpan
static member Zero = { timeSpan = TimeSpan(0L) }
let dateRange =
let endDate = System.DateTime.Parse("6/1/2010")
let startDate = System.DateTime.Parse("5/1/2010")
let oneDay = { timeSpan = System.TimeSpan(1,0,0,0) }
seq {
for date in startDate .. oneDay .. endDate do
if MyDateClass.IsBusinessDay(date) then yield date
}
The arithemetic difference between two DateTime objects in .NET is always a TimeSpan, that's your first problem. And if you had a TimeSpan, it wouldn't implement IEnumerable<>, so can't be used as a sequence. You can write your own sequence expression, though:
let rec dates (fromDate:System.DateTime) (toDate:System.DateTime) = seq {
if fromDate <= toDate then
yield fromDate
yield! dates (fromDate.AddDays(1.0)) toDate
}
You use it to create a sequence with all the dates in range, and then filter the result:
let result = dates startDate endDate |> Seq.filter (fun dt -> IsBusinessDate(dt))