How to Convert String(yyyy-mm-dd) to date format - blackberry-jde

I have find the date in (yyyy-mm-dd) format Like this 2011-09-20.
Please give me the solution to convert this string to Date.
I want to add this date to the calender event in blackberry.

I'm not sure I fully understand your question. If you are trying to get the current date, use this code:
java.util.Date today = new java.util.Date();
to get the current date, then use
today.toString()
to get the info such as day, month, and year. (Check out the BlackBerry API's for this)
If you are trying to set a date object, I would probably use Calendar.
Here is an example of my code where I create a Calendar object based on a specified date, then add it to the BlackBerry calendar
PIM pim = PIM.getInstance();
try {
String _date = date.getText();
int _month = Integer.parseInt(_date.substring(0, _date.indexOf('/')));
_date = _date.substring(_date.indexOf('/') + 1);
int _day = Integer.parseInt(_date.substring(0, _date.indexOf('/')));
_date = _date.substring(_date.indexOf('/') + 1);
int _year = Integer.parseInt(_date.substring(0, _date.indexOf('/')));
_year = _year + 2000;
EventList events = (EventList) pim.openPIMList(PIM.EVENT_LIST, PIM.READ_WRITE);
Event event = events.createEvent();
event.addString(Event.SUMMARY, PIMItem.ATTR_NONE, title.getText());
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, _year);
//this of course seems like a terrible way to set the months, but the BlackBerry
//api wants the month in this format
if(_month == 1)
cal.set(Calendar.MONTH, Calendar.JANUARY);
if(_month == 2)
cal.set(Calendar.MONTH, Calendar.FEBRUARY);
if(_month == 3)
cal.set(Calendar.MONTH, Calendar.MARCH);
if(_month == 4)
cal.set(Calendar.MONTH, Calendar.APRIL);
if(_month == 5)
cal.set(Calendar.MONTH, Calendar.MAY);
if(_month == 6)
cal.set(Calendar.MONTH, Calendar.JUNE);
if(_month == 7)
cal.set(Calendar.MONTH, Calendar.JULY);
if(_month == 8)
cal.set(Calendar.MONTH, Calendar.AUGUST);
if(_month == 9)
cal.set(Calendar.MONTH, Calendar.SEPTEMBER);
if(_month == 10)
cal.set(Calendar.MONTH, Calendar.OCTOBER);
if(_month == 11)
cal.set(Calendar.MONTH, Calendar.NOVEMBER);
if(_month == 12)
cal.set(Calendar.MONTH, Calendar.DECEMBER);
cal.set(Calendar.DATE, _day);
cal.set(Calendar.HOUR_OF_DAY, 17);
cal.set(Calendar.MINUTE, 15);
event.addDate(Event.START, PIMItem.ATTR_NONE, cal.getTime().getTime());
cal.set(Calendar.HOUR_OF_DAY, 21);
event.addDate(Event.END, PIMItem.ATTR_NONE, cal.getTime().getTime());
event.addString(BlackBerryEvent.LOCATION, PIMItem.ATTR_NONE, address.getText());
event.addString(Event.NOTE, PIMItem.ATTR_NONE, description.getText());
event.commit();
Dialog.alert(title.getText() + " was added to your calendar.");
} catch (PIMException e) {
// TODO Auto-generated catch block
System.out.println(e.getMessage());
e.printStackTrace();
}
}
Good Luck!

Related

Processing save table with AM or PM in file name

I have a project where I have data come in via the serial port every 15 minutes. I am using processing to read this data and save it as a CSV.
I would like for a new file to be created every 12 hours. However, when the file switches from AM to PM the entire row gets saved in the PM file (all the previous AM values)
How can I reset the table and start saving to a new file?
saveTable(dataTable, fileName());
dataTable.clearRows();
I tried this but it just clears the CSV file.
String fileName() {
String fileName = "";
String month = "";
String day = "";
int m = month();
int d = day();
if (d < 10) {
day = str(d);
day = "-0" + day;
} else {
day = "-" + str(d);
}
switch(m) {
case 1:
month = "-JAN";
break;
case 2:
month = "-FEB";
break;
case 3:
month = "-MAR";
break;
case 4:
month = "-APR";
break;
case 5:
month = "-MAY";
break;
case 6:
month = "-JUN";
break;
case 7:
month = "-JUL";
break;
case 8:
month = "-AUG";
break;
case 9:
month = "-SEP";
break;
case 10:
month = "-OCT";
break;
case 11:
month = "-NOV";
break;
case 12:
month = "-DEC";
break;
}
if (hour() >= 12) {
hour = "-PM";
} else {
hour = "-AM";
}
fileName = "SensorData_" + str(year()) + month + day + hour + ".csv";
return fileName;
}
Update: Code for collecting and saving data
void serialEvent(Serial myPort) {
if (myPort.available() > 0) {
String serialDataString = myPort.readString();
if (serialDataString != null) {
serialDataString = trim(serialDataString);
float[] sensorData = float(split(serialDataString, ','));
TableRow newRow = dataTable.addRow();
if (sensorData.length == 4) {
temperature = sensorData[0];
humidity = sensorData[1];
moisture = sensorData[2];
int packet = int(sensorData[3]);
if (packet < 10) {
packets = "00" + str(packet);
} else if (packet < 100) {
packets = "0" + str(packet);
}
String time = str(hour()) + ":" + str(minute()) + ":" + str(second());
String date = str(month()) + "/" + str(day());
newRow.setFloat("Temperature", temperature);
newRow.setFloat("Humidity", humidity);
newRow.setFloat("Moisture", moisture);
newRow.setString("Time", time);
newRow.setString("Date", date);
}
saveTable(dataTable, fileName());
}
}
}
In comments you've mentioned
Clearing after a save does not work as expected,
To clarify, what I meant is, if you call clearRows(), previous data will be erased. Saving before clearRows() should save previous data only, saving after clearRows() should only save current data.
I wrote a basic sketch and to me it looks that this works as expected:
void setup() {
// make new table, add 3 cols
Table dataTable = new Table();
dataTable.addColumn();
dataTable.addColumn();
dataTable.addColumn();
// add 1, 2, 3
TableRow newRow = dataTable.addRow();
newRow.setInt(0, 1);
newRow.setInt(1, 2);
newRow.setInt(2, 3);
// save to disk (expecting 1, 2, 3)
saveTable(dataTable,"test1.csv");
// print (expecting 1, 2, 3)
dataTable.print();
// completely clear table
dataTable.clearRows();
// add 4, 5, 6
newRow = dataTable.addRow();
newRow.setInt(0, 4);
newRow.setInt(1, 5);
newRow.setInt(2, 6);
// save again (expecting 4, 5, 6)
saveTable(dataTable,"test2.csv");
// print (expecting, 4, 5, 6)
dataTable.print();
}
(It's also nice that saveTable() appends data (and doesn't overwrite data) in this case.)
This is how I understand how/when data flows in your setup:
Arduino sends data over serial every 15 minutes. You haven't specified if the Arduino has a real time clock (RTC) and the code there uses it to only output data every 15 minutes on the clock (e.g. at :00, :15, :30, :45 past the hour, every hour). The assumption is there is no realtime clock and you're either using delay() or millis() so the actual time data gets sent out is relative to when the Arduino was powered.
When Processing sketch starts, it reads this serial data (meaning any prior data is loest). The assumption is there is no time sync between Arduino and Processing. The first row of data from Arduino comes at Arduino's next 15 minute (not Processing's) after the sketch was started.
The issue you might be experiencing based on your short snippet,
saveTable(dataTable, fileName());
dataTable.clearRows();
if it gets called in serialEvent() is that you'll loose data.
(Confusingly, it doesn't like you're calling clearRows() from serialEvent() ?)
One idea I can think is having some sort of event when the switch from AM/PM to then (first save any accumated data with the previous filename), then clear the the table and update the filename, otherwise (in serial event, save the data with the same filename).
A hacky approach is, once the AM/PM suffixed timestamp is generated to check if this suffix changes and only update filename/clear rows when this change occurs (e.g. manually "debouncing").
Here's a rough sketch to illustrate the idea:
Serial myPort;
float temperature, humidity, moisture;
Table dataTable = new Table();
String packets;
int packet;
boolean isAM,wasAM;
String tableFileName;
public void setup() {
textSize(14);
try{
myPort = new Serial(this, "COM4", 9600);
myPort.bufferUntil('\n');
}catch(Exception e){
println("Error opening Serial port!\nDouble check the Serial port is connected via USB, the port name is correct and the port istn't already open in Serial Monitor");
e.printStackTrace();
}
tableFileName = "SensorData_" + getDateStampString() + ".csv";
}
public void draw() {
background(255);
String sensorText = String.format("Temperature: %.2f Humidity: %.2f Moisture: %.2f", temperature, humidity, moisture);
float textWidth = textWidth(sensorText);
float textX = (width - textWidth) / 2;
fill(255);
rect(textX - 10, 14, textWidth + 20, 21);
fill(0);
text(sensorText, textX, 30);
// get an update date string
String dateStamp = getDateStampString();
// check AM/PM switch and update
isAM = dateStamp.endsWith("AM");
if(!wasAM && isAM){
println("changed PM to AM");
updateTableAMPM(dateStamp);
// update previous state for debouncing
wasAM = true;
}
if(wasAM && !isAM){
println("changed AM to PM");
updateTableAMPM(dateStamp);
wasAM = true;
}
}
public void updateTableAMPM(String dateStamp){
// saves current table (old filename): we're vaing data before the AM/PM switch
saveDataTable();
// clear rows so next 12 cycle starts fresh
dataTable.clearRows();
// update filename (for next save (serialEvent) to use)
tableFileName = "SensorData_" + dateStamp + ".csv";
}
public String getDateStampString(){
return new SimpleDateFormat("yyyy-MMM-dd-aa").format(new Date());
}
public void saveDataTable(){
saveTable(dataTable, tableFileName);
println("saved",tableFileName);
}
public void serialEvent(Serial myPort) {
if (myPort.available() > 0) {
String serialDataString = myPort.readString();
if (serialDataString != null) {
serialDataString = trim(serialDataString);
float[] sensorData = PApplet.parseFloat(split(serialDataString, ','));
TableRow newRow = dataTable.addRow();
if (sensorData.length == 4) {
temperature = sensorData[0];
humidity = sensorData[1];
moisture = sensorData[2];
int packet = PApplet.parseInt(sensorData[3]);
if (packet < 10) {
packets = "00" + str(packet);
} else if (packet < 100) {
packets = "0" + str(packet);
}
String time = str(hour()) + ":" + str(minute()) + ":" + str(second());
String date = str(month()) + "/" + str(day());
newRow.setFloat("Temperature", temperature);
newRow.setFloat("Humidity", humidity);
newRow.setFloat("Moisture", moisture);
newRow.setString("Time", time);
newRow.setString("Date", date);
}
// save data, but don't change the filename
saveDataTable();
}
}
}
Note the above isn't tested (so might contain errors), but hopefully it illustrates the ideas aforementioned.
(One minor note on packets (which I'm unsure where it's used): you can use nf() to easily pad a number with zeros (There are similar functions like nfc(), nfp(), nfs())).
Another option (similar to what I've mentioned in comments) is to use java utilities to call a function after a set time (e.g. the difference in time since the start of the sketch until either noon or midnight, whichever comes first), to then repeat at 12 hour intervals. You can check out TimerTask, or if your familiar with setTimeout in JS you can try this Thread based WIP setTimeout Processing workaround.

How to sort a list of timings like this [ 2:00 AM , 10:00 AM , 6:00 PM ] in dart-flutter?

I'm working on a flutter app, one of its features is to add your drug dose timings to get a reminder to take your drug. and I need to sort the timings to get the 'next_dose' to appear here :
https://drive.google.com/file/d/1j5KrRbDj0J28_FrMKy7dazk4m9dw202d/view?usp=sharing
this is an example of the list which I want to sort
[8:30 AM, 3:30 PM, 9:30 AM, 7:00 AM]
function I made to get the greater between 2 timings
int greater(String element,String element2){
var hour = element.toString().split(':')[0];
var hour2 = element2.toString().split(':')[0];
var minute = element.toString().split(':')[1].split(' ')[0];
var minute2 = element2.toString().split(':')[1].split(' ')[0];
var day = element.toString().split(':')[1].split(' ')[1];
var day2 = element2.toString().split(':')[1].split(' ')[1];
if(day == 'AM' && day2 == 'PM')
return -1;
else if(day2 == 'AM' && day == 'PM')
return 1;
else if(day == day2)
{
if(int.parse(hour) > int.parse(hour2)) return -1;
else if(int.parse(hour) < int.parse(hour2)) return 1;
else{
if(int.parse(minute) > int.parse(minute2))
return 1;
else
return -1;
}
}
here I tried to use the function to sort the list'Dose'
dose.sort((a,b)=>greater(a,b));
Instead of creating a sort callback with a lot of complicated logic, it'd be simpler if the callback parsed the time strings either into an int or into a DateTime object and compared those. For example:
/// Parses a time of the form 'hh:mm AM' or 'hh:mm PM' to a 24-hour
/// time represented as an int.
///
/// For example, parses '3:30 PM' as 1530.
int parseTime(String time) {
var components = time.split(RegExp('[: ]'));
if (components.length != 3) {
throw FormatException('Time not in the expected format: $time');
}
var hours = int.parse(components[0]);
var minutes = int.parse(components[1]);
var period = components[2].toUpperCase();
if (hours < 1 || hours > 12 || minutes < 0 || minutes > 59) {
throw FormatException('Time not in the expected format: $time');
}
if (hours == 12) {
hours = 0;
}
if (period == 'PM') {
hours += 12;
}
return hours * 100 + minutes;
}
void main() {
var list = ['8:30 AM', '3:30 PM', '9:30 AM', '7:00 AM'];
list.sort((a, b) => parseTime(a).compareTo(parseTime(b)));
print(list); // Prints: [7:00 AM, 8:30 AM, 9:30 AM, 3:30 PM]
}
Alternatively, you can use package:intl and DateFormat.parse to easily parse strings into DateTime objects.
Here we get the time slot by passing the start and end time duration.
List<String> createTimeSlot(
Duration startTime, Duration endTime, BuildContext context,
{Duration step = const Duration(minutes: 30)} // Gap between interval
) {
var timeSlot = <String>[];
var hourStartTime = startTime.inHours;
var minuteStartTime = startTime.inMinutes.remainder(60);
var hourEndTime = endTime.inHours;
var minuteEndTime = endTime.inMinutes.remainder(60);
do {
timeSlot.add(TimeOfDay(hour: hourStartTime, minute: minuteStartTime)
.format(context));
minuteStartTime += step.inMinutes;
while (minuteStartTime >= 60) {
minuteStartTime -= 60;
hourStartTime++;
}
} while (hourStartTime < hourEndTime ||
(hourStartTime == hourEndTime && minuteStartTime <= minuteEndTime));
debugPrint("Number of slot $timeSlot");
return timeSlot;
}
Function call
createTimeSlot(Duration(hours: 1, minutes: 30),
Duration(hours: 3, minutes: 30), context);
Output:
Number of slot [1:30 AM, 2:00 AM, 2:30 AM, 3:00 AM, 3:30 AM]

Calculating Total Job Experience

I need to calculate the total job experience as year value. Users add experiences with starting and ending dates to their resumes, just like Linkedin. But there is no any certain pattern. For instance;
A user may have a resume like that;
Experience 2
08.2012 - 01.2015
Experience 1
01.2011 - 01.2013
The user started their second experience while the first hasn't finished yet. So resumes may have many overlapping experiences. Overlapping also may occur between more than 2. So I need to consider many cases.
I tried to visualise the experience and year relationship for you.
I just need to develop an algorithm covering all the cases for this issue.
Sort by start date
start at the beginning and accumulate overlapping experiences (i.e. treat as one)
e.g. (Jan 2012, Jan 2015), (Jan 2014, Dec 2016) overlap, so we treat it as a single experience
This "super experience" begins at the start of the first, and ends at the end of the last; (Jan 2012, Dec 2016)
This is assuming that there can be gaps in experience, so we don't want to treat the entire history as one long "super experience"
I just did this
Sort the experiences by Begin Date.
Use 2 variables, that represent the begin of work and the end called Begin
and End.
If end and begin are empty it means that is the first date, we filled with the first experience, and we calculate the month count between the begin and end of this experience and and then this months count add to our months accumulator.
if the end and begin date of next experience is not between our begin and end variables, we calculate the months count from the experience and then add it to our accumulator.
If the begin date of next experience is between our begin and end variables, we calculate the months count between our end date as begin and the experience end date as end, and then that difference add to our months accumulator.
as you can see every time we set our new end if the experience end is greater than ours.
at the end we get the months, if you divide by 12 you get the years exactly you can round it if you want
NOTE: I Mapped the experiences, if not have End it means that is equivalent to NOW
you can see all the code below
function monthDiff(d1, d2) {
var m = (d1.getFullYear() - d2.getFullYear()) * 12 +
(d1.getMonth() - d2.getMonth());
if (d1.getDate() < d2.getDate()) --m;
return m;
}
function dateCheck(from, to, check) {
var fDate, lDate, cDate;
fDate = Date.parse(from);
lDate = Date.parse(to);
cDate = Date.parse(check);
if (cDate <= lDate && cDate >= fDate) {
return true;
}
return false;
}
function calculateYearsOfExperience(experiences) {
if (!experiences) return 0;
let months = 0;
let now = new Date();
let sorted = experiences
.sort((a, b) => {
return new Date(a.begin) - new Date(b.begin);
})
.map(experience => {
if (!experience.end) experience.end = now;
return { begin: experience.begin, end: experience.end };
});
let begin;
let end;
for (var i in sorted) {
let dif = 0;
if (!end && !begin) {
dif = monthDiff(sorted[i].begin, sorted[i].end);
begin = sorted[i].begin;
end = sorted[i].end;
} else if (
!dateCheck(begin, end, sorted[i].begin) &&
!dateCheck(begin, end, sorted[i].end)
) {
dif = monthDiff(sorted[i].begin, sorted[i].end);
end = sorted[i].end;
} else if (dateCheck(begin, end, sorted[i].begin)) {
dif = monthDiff(end, sorted[i].end);
end = sorted[i].end;
}
months += dif;
}
return months / 12;
}
experiences = [
# (start date, end date),
(date(2012, 8, 1), date(2015, 1, 30)),
(date(2011, 1, 1), date(2013, 1, 30))
]
print(sum((end_date - start_date).days/365 for start_date, end_date in experiences))
Create an array full of 0. Element range: Start would be the very first hire date, last would be current date / last working date. For every year you have 12 elements that can be 0/1. After filling in the array count the elements that have the value 1. If you need to take into account the overlapping then ignore the 0/1 part and give +1 for every month worked.
This is c# code, but you'll get the steps in algorithm. It returns total number of months which you can convert into year and month format.
var sortedList = expList
.OrderBy(a => a.start_year)
.ThenBy(a => a.start_month)
.ThenBy(a => a.end_year)
.ThenBy(a => a.end_month)
.ToList();
int totalMonths = 0;
int totalDays = 0;
DateTime? prevEndDate = null;
foreach (var exp in sortedList)
{
if(exp.start_month != null && exp.start_year != null)
{
var startDate = new DateTime((int)exp.start_year, (int)exp.start_month, 1);
var endDate = (bool)exp.is_present ?
DateTime.Now :
new DateTime((int)exp.end_year, (int)exp.end_month, 1);
if (prevEndDate != null && prevEndDate > startDate)
{
startDate = (DateTime)prevEndDate;
}
var timespan = endDate.Subtract(startDate);
var tempdate = DateTime.MinValue + timespan;
totalMonths = totalMonths + (tempdate.Year - 1) * 12 + tempdate.Month - 1;
totalDays = totalDays + tempdate.Day - 1;
prevEndDate = endDate;
}
}
if (totalDays > 0)
{
totalMonths = totalMonths + 1;
}
public static function YearCalculation($id=null) {
if($id=="")
$employee_id=#Auth::guard('web_employee')->user()->id;
else
$employee_id=$id;
$employee_exp = Experience::where('employee_id',$employee_id)->orderBy("experience_from","ASC")->get();
//print_r($employee_exp ); exit;
$years = 0;
$months = 0;
$days = 0;
$sum=0;
$lastfromdate=0;
$last_endDate=0;
if(#$employee_exp!=null){
foreach(#$employee_exp as $experience){
$fdate = #$experience->experience_from;
if($experience->is_current==0){
$tdate = #$experience->experience_to;
}else{
$tdate = date("Y-m-d");
}
if($last_endDate != date("Y-m-d") ){
if( $fdate < $last_endDate ){
$start_date1 = strtotime($last_endDate);
$end_date1 = strtotime($tdate);
$interval1 = ($end_date1 - $start_date1)/60/60/24;
$sum +=$interval1;
}
else{
$start_date2 = strtotime($fdate);
$end_date2 = strtotime($tdate);
$interval2 = ($end_date2 - $start_date2)/60/60/24;
$sum +=$interval2;
}
if($last_endDate < $tdate){
$last_endDate=$tdate;
}
}
}
$years = ($sum / 365) ;
$years = floor($years);
$month = ($sum % 365) / 30.4166;
$month = floor($month);
$days = ($sum % 365) % 30.4166;
//$total=date_diff(0,$tmstamp);
//$total_year=$years.' years, ' .$month.' months and '.$days.($days==1?' day':' days');
$total_year=($years==0?'':($years==1?$years.' year':$years.' years and ')) .($month==0?'':($month==1?$month.($days>1?'+':'').' month':$month.($days>1?'+':'').' months'));
return $total_year;
}
}

YouMax video upload date

I've been using the YouMax plugin which enables you to embed your YouTube channel on your website. However, I am having problems as it displays the uploaded date in months and years. I'd like it to display days, weeks, months and years.
You can view the source code here http://jsfiddle.net/wCKKU/
I believe that its this that needs adjusting to make it calculate in day, weeks, months and years.
function getDateDiff(timestamp) {
if (null == timestamp || timestamp == "" || timestamp == "undefined") return "?";
var splitDate = ((timestamp.toString().split('T'))[0]).split('-');
var d1 = new Date();
var d1Y = d1.getFullYear();
var d2Y = parseInt(splitDate[0], 10);
var d1M = d1.getMonth();
var d2M = parseInt(splitDate[1], 10);
var diffInMonths = (d1M + 12 * d1Y) - (d2M + 12 * d2Y);
if (diffInMonths <= 1) return "1 month";
else if (diffInMonths < 12) return diffInMonths + " months";
var diffInYears = Math.floor(diffInMonths / 12);
if (diffInYears <= 1) return "1 year";
else if (diffInYears < 12) return diffInYears + " years"
}
You could modify the plugin by a small block of code in the middle of the function:
var d2M = parseInt(splitDate[1], 10); // this line is already there
var d1D = d1.getDate();
var d2D = parseInt(splitDate[2],10);
var diffInDays = (d1D + 30 *d1M + 12 * d1Y) - (d2D + 30 *d2M + 12 *d2Y);
if (diffInDays < 2) return "1 day";
else if (diffInDays < 7) return diffInDays+" days";
else if (diffInDays > 7 && diffInDays < 14) return "1 week";
else if (diffInDays > 14 && diffInDays < 30) return Math.floor(diffInDays / 7) + " weeks";
var diffInMonths = (d1M + 12 * d1Y) - (d2M + 12 * d2Y); // this line is already there
Note that this isn't a particularly elegant way to handle the issue, but it matches the coding style the plugin is already using, and at least won't break anything else.
Also, as a side comment, if you're modifying the plugin code you'll want to fix a bug in it at the same time. Getting the current month should look like this:
var d1M = d1.getMonth() + 1;
This is because in Javascript, the getMonth() function returns the month on a zero-based index, and your math won't be reliable unless you switch it to a one-based index.

Fuzzy date algorithm

I'm looking for a fuzzy date algorithm. I just started writing one and realised what a tedious task it is. It quickly degenerated into a lot of horrid code to cope with special cases like the difference between "yesterday", "last week" and "late last month" all of which can (in some cases) refer to the same day but are individually correct based on today's date.
I feel sure there must be an open source fuzzy date formatter but I can't find it. Ideally I'd like something using NSDate (OSX/iPhone) and its formatters but that isn't the difficult bit. Does anyone know of a fuzzy date formatter taking any time period relative to now and returning a string like (but not limited to):
a few moments ago
in the last five minutes
earlier today
this morning
last night
last week
last wednesday
early last month
june last year
a couple of years ago
In an ideal world I'd like the string to be as rich as possible (i.e. returning random variants on "Just a moment ago" such as "just now").
Clarification. I'm looking for something more subtle than basic buckts and strings. I want something that knows "yesterday" and "last wednesday" can both refer to the same period but only one is correct when today is Thursday.
There is a property in NSDateFormatter - "doesRelativeDateFormatting". It appears only in 10.6/iOS4.0 and later but it will format a date into a relative date in the correct locale.
From Apple's Documentation:
If a date formatter uses relative date
formatting, where possible it replaces
the date component of its output with
a phrase—such as “today” or
“tomorrow”—that indicates a relative
date. The available phrases depend on
the locale for the date formatter;
whereas, for dates in the future,
English may only allow “tomorrow,”
French may allow “the day after the
day after tomorrow,” as illustrated in
the following example.
Code
The following is code that will print out a good number of the relative strings for a given locale.
NSLocale *locale = [NSLocale currentLocale];
// NSLocale *locale = [[[NSLocale alloc] initWithLocaleIdentifier:#"fr_FR"] autorelease];
NSDateFormatter *relativeDateFormatter = [[NSDateFormatter alloc] init];
[relativeDateFormatter setTimeStyle:NSDateFormatterNoStyle];
[relativeDateFormatter setDateStyle:NSDateFormatterMediumStyle];
[relativeDateFormatter setDoesRelativeDateFormatting:YES];
[relativeDateFormatter setLocale:locale];
NSDateFormatter *normalDateFormatter = [[NSDateFormatter alloc] init];
[normalDateFormatter setTimeStyle:NSDateFormatterNoStyle];
[normalDateFormatter setDateStyle:NSDateFormatterMediumStyle];
[normalDateFormatter setDoesRelativeDateFormatting:NO];
[normalDateFormatter setLocale:locale];
NSString * lastUniqueString = nil;
for ( NSTimeInterval timeInterval = -60*60*24*400; timeInterval < 60*60*24*400; timeInterval += 60.0*60.0*24.0 )
{
NSDate * date = [NSDate dateWithTimeIntervalSinceNow:timeInterval];
NSString * relativeFormattedString = [relativeDateFormatter stringForObjectValue:date];
NSString * formattedString = [normalDateFormatter stringForObjectValue:date];
if ( [relativeFormattedString isEqualToString:lastUniqueString] || [relativeFormattedString isEqualToString:formattedString] )
continue;
NSLog( #"%#", relativeFormattedString );
lastUniqueString = relativeFormattedString;
}
Notes:
A locale is not required
There are
not that many substitutions for
English. At the time of writing there
are: "Yesterday, Today, Tomorrow".
Apple may include more in the future.
It's fun to change the locale and see
what is available in other languages
(French has a few more than English,
for example)
If on iOS, you might want to subscribe to UIApplicationSignificantTimeChangeNotification
Interface Builder
You can set the "doesRelativeDateFormatting" property in Interface Builder:
Select your NSDateFormatter and
choose the "Identity Inspector" tab
of the Inspector Palette (the last
one [command-6]).
Under the sub-section named "User
Defined Runtime Attributes", you can
add your own value for a key on the selected object (in this case, your NSDateFormatter instance). Add
"doesRelativeDateFormatting", choose
a "Boolean" type, and make sure it's
checked.
Remember: It may look like it didn't work at all, but that might because there are only a few substituted values for your locale. Try at least a date for Yesterday, Today, and Tomorrow before you decide if it's not set up right.
This question should get you started. It has the code this very site uses to calculate its relative time. It may not have the specific ranges you want, but they are easy enough to add once you got it setup.
You might want to look at Rail's distance_of_time_in_words function in date_helper.rb, which I've pasted below.
# File vendor/rails/actionpack/lib/action_view/helpers/date_helper.rb, line 59
def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {})
from_time = from_time.to_time if from_time.respond_to?(:to_time)
to_time = to_time.to_time if to_time.respond_to?(:to_time)
distance_in_minutes = (((to_time - from_time).abs)/60).round
distance_in_seconds = ((to_time - from_time).abs).round
I18n.with_options :locale => options[:locale], :scope => 'datetime.distance_in_words''datetime.distance_in_words' do |locale|
case distance_in_minutes
when 0..1
return distance_in_minutes == 0 ?
locale.t(:less_than_x_minutes, :count => 1) :
locale.t(:x_minutes, :count => distance_in_minutes) unless include_seconds
case distance_in_seconds
when 0..4 then locale.t :less_than_x_seconds, :count => 5
when 5..9 then locale.t :less_than_x_seconds, :count => 10
when 10..19 then locale.t :less_than_x_seconds, :count => 20
when 20..39 then locale.t :half_a_minute
when 40..59 then locale.t :less_than_x_minutes, :count => 1
else locale.t :x_minutes, :count => 1
end
when 2..44 then locale.t :x_minutes, :count => distance_in_minutes
when 45..89 then locale.t :about_x_hours, :count => 1
when 90..1439 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round
when 1440..2879 then locale.t :x_days, :count => 1
when 2880..43199 then locale.t :x_days, :count => (distance_in_minutes / 1440).round
when 43200..86399 then locale.t :about_x_months, :count => 1
when 86400..525599 then locale.t :x_months, :count => (distance_in_minutes / 43200).round
when 525600..1051199 then locale.t :about_x_years, :count => 1
else locale.t :over_x_years, :count => (distance_in_minutes / 525600).round
end
end
end
So, here is the category I wrote on NSDate for those who are still interested. The problem is one of those that becomes a little quixotic. It is basically a huge switch statment (although I implemented it in a series of cascading if()s to keep it more readable.
For each time period I then select from a random set of ways of telling the time.
All in all, this delighted a few of our users but I'm not sure it was worth the effort.
NSTimeInterval const kTenSeconds = (10.0f );
NSTimeInterval const kOneMinute = (60.0f);
NSTimeInterval const kFiveMinutes = (5.0f*60.0f);
NSTimeInterval const kFifteenMinutes = (15.0f*60.0f) ;
NSTimeInterval const kHalfAnHour = (30.0f*60.0f) ;
NSTimeInterval const kOneHour = 3600.0f; // (60.0f * 60.0f);
NSTimeInterval const kHalfADay = (3600.0f * 12.0f);
NSTimeInterval const kOneDay = (3600.0f * 24.0f);
NSTimeInterval const kOneWeek = (3600.0f * 24.0f * 7.0f);
#implementation NSDate (Fuzzy)
-(NSString*)fuzzyStringRelativeToNow;
{
static NSArray* secondsStrings;
static NSArray* minuteStrings;
static NSArray* fiveMinuteStrings;
static NSArray* halfHourStrings;
static NSArray* earlyMonthStrings;
NSTimeInterval timeFromNow = [self timeIntervalSinceNow];
if((timeFromNow < 0)) // In the past
{
timeFromNow = - timeFromNow;
if ( (timeFromNow < kTenSeconds))
{
if(!secondsStrings)
{
secondsStrings = [[NSArray arrayWithObjects:#"just now",
//#"a few seconds ago",
//#"right this instant",
#"moments ago",
nil] retain];
}
unsigned int index = random() % ([secondsStrings count] - 1);
return [secondsStrings objectAtIndex:index];
}
if ( (timeFromNow < kOneMinute))
{
if(!minuteStrings)
{
minuteStrings = [[NSArray arrayWithObjects:#"just now",
#"very recently",
#"in the last minute",
nil] retain];
}
unsigned int index = random() % ([minuteStrings count] - 1);
return [minuteStrings objectAtIndex:index];
}
if (timeFromNow < kFiveMinutes)
{
if(!fiveMinuteStrings)
{
fiveMinuteStrings = [[NSArray arrayWithObjects:#"just now",
#"very recently",
//#"in the last minute",
#"a few minutes ago",
//#"in the last five minutes",
nil] retain];
}
unsigned int index = random() % ([fiveMinuteStrings count] - 1);
return [fiveMinuteStrings objectAtIndex:index];
}
if (timeFromNow < kFifteenMinutes)
{
return #"in the last 15 minutes";
}
if (timeFromNow < kHalfAnHour)
{
if(!halfHourStrings)
{
halfHourStrings = [[NSArray arrayWithObjects:#"in the last half hour",
//#"in the last half an hour",
#"in the last 30 minutes",
//#"about half an hour ago",
#"fairly recently",
nil] retain];
}
unsigned int index = random() % ([halfHourStrings count] - 1);
return [halfHourStrings objectAtIndex:index];
}
if (timeFromNow < kOneHour)
{
return #"in the last hour";
}
if ((timeFromNow < (kOneHour + kFiveMinutes)) && (timeFromNow > (kOneHour - kFiveMinutes)))
{
return #"about an hour ago";
}
if((timeFromNow < ((kOneHour*2.0f) + kFiveMinutes ))&& (timeFromNow > ((kOneHour*2.0f) - kFiveMinutes)))
{
return #"a couple of hours ago";
}
// Now we're over an hour, we need to calculate a few specific dates to compare against
NSDate *today = [NSDate date];
NSCalendar *gregorian = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
NSUInteger unitFlags = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit;
NSDateComponents* todayComponents = [gregorian components:unitFlags fromDate:today];
todayComponents.hour = 12;
NSDate* noonToday = [gregorian dateFromComponents:todayComponents];
NSTimeInterval timeSinceNoonToday = [self timeIntervalSinceDate:noonToday];
if (timeSinceNoonToday > 0) // sometime since noon
{
if (timeSinceNoonToday > kOneHour * 9) // i.e. after 9pm today
return #"earlier tonight";
if (timeSinceNoonToday > kOneHour * 7) // i.e. after 7pm today
return #"earlier this evening";
if (timeSinceNoonToday < kOneHour * 1) // between noon and 1pm
return #"early this afternoon";
return #"this afternoon";
}
NSTimeInterval timeSinceMidnight = kHalfADay -timeSinceNoonToday; // Note sign is reversed.
if ((timeSinceNoonToday < 0) & (timeSinceNoonToday > -kHalfADay)) // between midnight and noon today
{
if (timeSinceMidnight < kFiveMinutes)
return #"around midnight";
if (timeSinceMidnight < kOneHour * 2) // up to 2am
return #"very early this morning";
if (timeSinceMidnight < kOneHour * 5) // up to 5am
return #"early this morning";
else if (timeSinceMidnight < kOneHour * 11)
return #"late this morning";
else
return #"this morning";
}
// NSTimeInterval timeSinceNoonYesterday = timeSinceNoonToday - kOneDay;
// timeSinceMidnight = -timeSinceMidnight;
if (timeSinceMidnight < kOneHour * 24) // not the day before...
{
if (timeSinceMidnight < kFiveMinutes)
return #"around midnight";
if (timeSinceMidnight < kFifteenMinutes)
return #"just before midnight";
if (timeSinceMidnight < kOneHour * 2) // after 10pm
return #"late last night";
if (timeSinceMidnight < kOneHour * 5) // After 7
return #"yesterday evening";
else if (timeSinceMidnight < kOneHour * 7)
return #"yesterday evening"; // after 5pm
else if (timeSinceMidnight < kOneHour * 7)
return #"yesterday evening"; // after 5pm
else if (timeSinceMidnight < kOneHour * 10)
return #"yesterday afternoon"; // after 5pm
else if (timeSinceMidnight < kOneHour * 12)
return #"early yesterday afternoon"; // before 1pm
else if (timeSinceMidnight < kOneHour * 13)
return #"late yesterday morning"; // after 11m
else if (timeSinceMidnight < kOneHour * 17)
return #"yesterday morning";
else
return #"early yesterday morning";
}
NSDateFormatter* formatter = [[[NSDateFormatter alloc] init] autorelease];
int integerSeconds = timeSinceMidnight;
int integerDay = kOneDay;
int secondsIntoDay = integerSeconds % integerDay;
NSString* formatString = #"last %#";
if (timeFromNow < kOneWeek)
{
if (secondsIntoDay < kFifteenMinutes)
formatString = #"around midnight on %#";
//else if (secondsIntoDay < kFifteenMinutes)
// formatString = #"just before midnight on %#";
else if (secondsIntoDay < kOneHour * 2) // after 10pm
formatString = #"late on %# night";
else if (secondsIntoDay < kOneHour * 5) // After 7
formatString = #"on %# evening";
else if (secondsIntoDay < kOneHour * 10)
formatString = #"on %# afternoon"; // after 5pm
else if (secondsIntoDay < kOneHour * 12)
formatString = #"early on %# afternoon"; // before 1pm
else if (secondsIntoDay < kOneHour * 13)
formatString = #"late on %# morning"; // after 11am
else if (secondsIntoDay < kOneHour * 17)
formatString = #"on %# morning";
else if (secondsIntoDay < kOneHour * 24) // not the day before...
formatString = #"early on %# morning";
[formatter setDateFormat:#"EEEE"]; /// EEEE is long format of day of the week see: http://unicode.org/reports/tr35/tr35-6.html#Date_Format_Patterns
return [NSString stringWithFormat:formatString, [formatter stringFromDate: self]];
}
//formatString = #"on %# the week before last";
/*if (secondsIntoDay < kOneHour * 2) // after 10pm
formatString = #"early on %# the week before last";
else if (timeSinceMidnight > kOneHour * 13)
formatString = #"late on %# the week before last"; // after 11m*/
//if (timeFromNow < kOneWeek * 2)
//{
// [formatter setDateFormat:#"EEE"]; /// EEE is short format of day of the week see: http://unicode.org/reports/tr35/tr35-6.html#Date_Format_Patterns
// return [NSString stringWithFormat:formatString, [formatter stringFromDate: self]];
//}
if (timeFromNow < kOneWeek * 2)
{
return #"the week before last";
}
NSDateComponents* myComponents = [gregorian components:unitFlags fromDate:self];
int monthsAgo = myComponents.month - todayComponents.month;
int yearsAgo = myComponents.year - todayComponents.year;
if (yearsAgo == 0)
{
if (monthsAgo == 0)
{
if(myComponents.day > 22)
return #"late this month";
if(myComponents.day < 7)
{
if(!earlyMonthStrings)
{
earlyMonthStrings = [[NSArray arrayWithObjects:#"earlier this month",
//#"at the beginning of the month",
#"early this month",
nil] retain];
}
unsigned int index = random() % ([earlyMonthStrings count] - 1);
return [earlyMonthStrings objectAtIndex:index];
}
return #"earlier this month";
}
if (monthsAgo == 1)
{
if(myComponents.day > 22)
return #"late last month";
if(myComponents.day < 7)
return #"early last month";
return #"last month";
}
formatString = #"in %# this year";
/*if(myComponents.day > 22)
formatString = #"late in %# this year";
if(myComponents.day < 7)
formatString = #"early in %# this year";*/
[formatter setDateFormat:#"MMMM"]; /// MMM is longformat of month see: http://unicode.org/reports/tr35/tr35-6.html#Date_Format_Patterns
return [NSString stringWithFormat:formatString, [formatter stringFromDate: self]];
}
if (yearsAgo == 1)
{
formatString = #"in %# last year";
/*if(myComponents.day > 22)
formatString = #"late in %# last year";
if(myComponents.day < 7)
formatString = #"late in %# last year";*/
[formatter setDateFormat:#"MMM"]; /// MMM is longformat of month see: http://unicode.org/reports/tr35/tr35-6.html#Date_Format_Patterns
return [NSString stringWithFormat:formatString, [formatter stringFromDate: self]];
}
// int daysAgo = integerSeconds / integerDay;
// Nothing yet...
[formatter setDateStyle:kCFDateFormatterMediumStyle];
//[formatter setTimeStyle:kCFDateFormatterShortStyle];
return [NSString stringWithFormat:#"on %#",[formatter stringFromDate: self]];
}
else
if(timeFromNow > 0) // The future
{
AICLog(kErrorLogEntry, #"FuzzyDates: Time marked as in the future: referenced date is %#, local time is %#", self, [NSDate date]);
return #"moments ago";
}
else
return #"right now"; // this seems unlikely.
return [self description]; // should never get here.
}
sorry it took so long to post this...
This is based on code in the Pretty and Humane date & time threads. I added handling for "last Monday, 5pm", because I like that more than x days ago. This handles past and future up to centuries. I am keen on the internationalization aspect so this needs a lot more work eventually. Calculations are in the local time zone.
public static class DateTimePretty
{
private const int SECOND = 1;
private const int MINUTE = 60 * SECOND;
private const int HOUR = 60 * MINUTE;
private const int DAY = 24 * HOUR;
private const int WEEK = 7 * DAY;
private const int MONTH = 30 * DAY;
private const int YEAR = 365;
const string now = "just now";
const string secondsFuture = "in {0} seconds", secondsPast = "{0} seconds ago";
const string minuteFuture = "in about a minute", minutePast = "about a minute ago";
const string minutesFuture = "in about {0} minutes", minutesPast = "about {0} minutes ago";
const string hourFuture = "in about an hour", hourPast = "about an hour ago";
const string hoursFuture = "in about {0} hours", hoursPast = "about {0} hours ago";
const string tomorrow = "tomorrow, {0}", yesterday = "yesterday, {0}";
const string nextDay = "{0}", nextWeekDay = "next {0}", lastDay = "last {0}";
//const string daysFuture = "in about {0} days", daysPast = "about {0} days ago";
const string weekFuture = "in about a week", weekPast = "about a week ago";
const string weeksFuture = "in about {0} weeks", weeksPast = "about {0} weeks ago";
const string monthFuture = "in about a month", monthPast = "about a month ago";
const string monthsFuture = "in about {0} months", monthsPast = "about {0} months ago";
const string yearFuture = "in about a year", yearPast = "about a year ago";
const string yearsFuture = "in about {0} years", yearsPast = "about {0} years ago";
const string centuryFuture = "in about a century", centuryPast = "about a century ago";
const string centuriesFuture = "in about {0} centuries", centuriesPast = "about {0} centuries ago";
/// <summary>
/// Returns a pretty version of the provided DateTime: "42 years ago", or "in 9 months".
/// </summary>
/// <param name="dateTime">DateTime in local time format, not Utc</param>
/// <returns>A pretty string</returns>
public static string GetPrettyDate(DateTime dateTime)
{
DateTime dateTimeNow = DateTime.Now;
bool isFuture = (dateTimeNow.Ticks < dateTime.Ticks);
var ts = isFuture ? new TimeSpan(dateTime.Ticks - dateTimeNow.Ticks) : new TimeSpan(dateTimeNow.Ticks - dateTime.Ticks);
double delta = ts.TotalSeconds;
if (delta < 10)
return now;
if (delta < 1 * MINUTE)
return isFuture ? string.Format(secondsFuture, ts.Seconds) : string.Format(secondsPast, ts.Seconds);
if (delta < 2 * MINUTE)
return isFuture ? minuteFuture : minutePast;
if (delta < 45 * MINUTE)
return isFuture ? string.Format(minutesFuture, ts.Minutes) : string.Format(minutesPast, ts.Minutes);
if (delta < 2 * HOUR)
return isFuture ? hourFuture : hourPast;
if (delta < 7 * DAY)
{
string shortTime = DateTimeFormatInfo.CurrentInfo.ShortTimePattern;
string shortWeekdayTime = "dddd, " + shortTime;
int dtDay = (int) dateTime.DayOfWeek;
int nowDay = (int) dateTimeNow.DayOfWeek;
if (isFuture)
{
if (dtDay == nowDay)
{
if (delta < DAY)
return string.Format(hoursFuture, ts.Hours);
else
return string.Format(nextWeekDay, dateTime.ToString(shortWeekdayTime));
}
else if (dtDay - nowDay == 1 || dtDay - nowDay == -6)
return string.Format(tomorrow, dateTime.ToString(shortTime));
else
return string.Format(nextDay, dateTime.ToString(shortWeekdayTime));
}
else
{
if (dtDay == nowDay)
{
if (delta < DAY)
return string.Format(hoursPast, ts.Hours);
else
return string.Format(lastDay, dateTime.ToString(shortWeekdayTime));
}
else if (nowDay - dtDay == 1 || nowDay - dtDay == -6)
return string.Format(yesterday, dateTime.ToString(shortTime));
else
return string.Format(lastDay, dateTime.ToString(shortWeekdayTime));
}
}
//if (delta < 7 * DAY)
// return isFuture ? string.Format(daysFuture, ts.Days) : string.Format(daysPast, ts.Days);
if (delta < 4 * WEEK)
{
int weeks = Convert.ToInt32(Math.Floor((double) ts.Days / 30));
if (weeks <= 1)
return isFuture ? weekFuture : weekPast;
else
return isFuture ? string.Format(weeksFuture, weeks) : string.Format(weeksPast, weeks);
}
if (delta < 12 * MONTH)
{
int months = Convert.ToInt32(Math.Floor((double) ts.Days / 30));
if (months <= 1)
return isFuture ? monthFuture : monthPast;
else
return isFuture ? string.Format(monthsFuture, months) : string.Format(monthsPast, months);
}
// Switch to days to avoid overflow
delta = ts.TotalDays;
if (delta < 100 * YEAR)
{
int years = Convert.ToInt32(Math.Floor((double) ts.TotalDays / 365.25));
if (years <= 1)
return isFuture ? yearFuture : yearPast;
else
return isFuture ? string.Format(yearsFuture, years) : string.Format(yearsPast, years);
}
else
{
int centuries = Convert.ToInt32(Math.Floor((double) ts.TotalDays / 365.2425));
if (centuries <= 1)
return isFuture ? centuryFuture : centuryPast;
else
return isFuture ? string.Format(centuriesFuture, centuries) : string.Format(centuriesPast, centuries);
}
}
}
I am not sure why you say it would be a horrid coding practice. Each of the return strings are actually a subset of the parent set, so you can quite elegantly do this in a if/elseif chain.
if timestamp < 5sec
"A moment ago"
elseif timestamp < 5min
"Few minutes ago"
elseif timestamp < 12hr && timestamp < noon
"Today Morning"
...
elseif timestamp < 1week
"Few days ago"
elseif timestamp < 1month
"Few weeks ago"
elseif timestamp < 6month
"Few Months ago"
...
else
"Really really long time ago"
In my experience these types of date generators are not "fuzzy" at all. In fact, they are just a bunch of if statements based bands of time. For example, any time less than 30 seconds is "moments ago", 360 to 390 days is "just a year ago", etc. Some of these will use the target date to calculate the special names (June, Wednesday, etc).
Sorry to dash an illusions you had.
needless to say (but i'll say it anyway) don't use a where loop that decrements 365 days per year even on 366 day leap years (or you'll find yourself in the ranks of the Zune developers)
here is a c# version:
http://tiredblogger.wordpress.com/2008/08/21/creating-twitter-esque-relative-dates-in-c/
I know expressing times like this has become quite popular lately, but please considering making it an option to switch been relative 'fuzzy' dates and normal absolute dates.
For example, it's useful to know that a comment was made 5 minutes ago, but it's less useful to tell me comment A was 4 hours ago and comment B was 9 hours ago when it's 11 AM and I'd rather know that comment A was written when someone woke up this morning and comment B was written by someone staying up late (assuming I know they are in my timezone).
--
EDIT: looking closer at your question you seem to have avoided this to some degree by referring to time of day instead of "X ago", but on the other hand, you may be giving a false impression if users are in different time zone, since your "this morning" may be in the middle of the night for the relevant user.
It might be cool to augment the times with relative time of day depending on the other user's timezone, but that assumes that users are willing to supply it and that it's correct.
I was not happy with the solution in the other question. So made my own using the Date time class. IMO, its cleaner. In my tests it worked like as I wanted. Hope this helps someone.
DateTime now = DateTime.Now;
long nowticks = now.Ticks;
long thenticks = dt.Ticks;
long diff = nowticks - thenticks;
DateTime n = new DateTime(diff);
if (n.Year > 1)
{
return n.Year.ToString() + " years ago";
}
else if (n.Month > 1)
{
return n.Month.ToString() + " months ago";
}
else if (n.Day > 1)
{
return n.Day.ToString() + " days ago";
}
else if (n.Hour > 1)
{
return n.Hour.ToString() + " hours ago";
}
else if (n.Minute > 1)
{
return n.Minute.ToString() + " minutes ago";
}
else
{
return n.Second.ToString() + " seconds ago";
}
This is almost always done using a giant switch statement and is trivial to implement.
Keep the following in mind:
Always test for the smallest time span first
Don't forget to keep your strings localizable.
You may find the source from timeago useful. The description of the plugin is "a jQuery plugin that makes it easy to support automatically updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago")."
It's essentially a JavaScript port of Rail's distance_of_time_in_words function crammed into a jQuery plugin.
My company has this .NET library that does some of what you want in that it does very flexible date time parsing (including some relative formats) but it only does non-relative outputs.
Check out Chrono for a Javascript heuristic date parser.
Chrono supports most date and time formats, such as:
Today, Tomorrow, Yesterday, Last Friday, etc
17 August 2013 - 19 August 2013
This Friday from 13:00 - 16.00
5 days ago
Sat Aug 17 2013 18:40:39 GMT+0900 (JST)
2014-11-30T08:15:30-05:30
https://github.com/wanasit/chrono

Resources