openhab / openhab-js

openHAB JavaScript Library for JavaScript Scripting Automation
https://www.openhab.org/addons/automation/jsscripting/
Eclipse Public License 2.0
38 stars 31 forks source link

[time] Time Utils #101

Closed rkoshak closed 2 years ago

rkoshak commented 2 years ago

[time] Time Utils

Description

Implements a number of utilities for working with date times.

Testing

I used the following in a MainUI Script to test this library .

var {testUtils} = require('openhab_rules_tools');
var {DateTimeType, DecimalType, QuantityType} = require('@runtime');
var OHItem = Java.type('org.openhab.core.items.Item');
var javaZDT = Java.type('java.time.ZonedDateTime');
var javaDur = Java.type('java.time.Duration');
var testItems = [];
var veryClose = time.Duration.ofMillis(30);
var prefix = 'TestToZDT';

var exceptionExpected = (runme) => {
  try {
    runme();
  }
  catch(e) {
    return true;
  }
  return false;
}

var testOhType = (name, type, state) => {
  items.replaceItem(name, type);
  testUtils.sleep(100);
  items.getItem(name).postUpdate(state);
  testUtils.sleep(100);
  testUtils.assert(items.getItem(name).state == state.toString());
  testItems.push(name);
}

try {
  console.info('Starting toZDT Tests')

  var testNum = 0;

  // Test 1: undefined or null
  console.info("Executing Test" + (++testNum));
  var now = time.ZonedDateTime.now();
  testUtils.assert(now.isClose(time.ZonedDateTime.now(), veryClose), 
                   'Test 1: Failed to convert undefined to now');

  // Test 2: passthrough
  console.info("Executing Test" + (++testNum));
  testUtils.assert(time.toZDT(now) === now,
                  'Test 2: Failed to passthrough time.ZDT');

  // Test 3: Java ZDT
  console.info("Executing Test" + (++testNum));
  testUtils.assert(time.toZDT(javaZDT.now()).isClose(time.toZDT(), veryClose),
                  'Test 3: Failed to convert Java ZDT');

  // Test 4: DateTimeType
  console.info("Executing Test" + (++testNum));
  testUtils.assert(time.toZDT(new DateTimeType()).isClose(time.toZDT(), veryClose),
                  'Test 4: Failed. to convert DateTimeType');

  // Test 5: JavaScript Date
  console.info("Executing Test" + (++testNum));
  testUtils.assert(time.toZDT(new Date()).isClose(time.toZDT(), veryClose),
                  'Test 5: Failed to convert JS Date');

  // Test 6: JavaScript Number
  console.info("Executing Test" + (++testNum));
  testUtils.assert(time.toZDT(10).isClose(time.toZDT().plus(veryClose), veryClose),
                  'Test 6: Failed. to convert number');

  // Test 7: Java Duration
  console.info("Executing Test" + (++testNum));
  testUtils.assert(time.toZDT(javaDur.ofMillis(10)).isClose(time.toZDT(10), veryClose),
                  'Test 7: Failed to convert a java Duration');

  // Test 8: time.Duration
  console.info("Executing Test" + (++testNum));
  testUtils.assert(time.toZDT(veryClose).isClose(time.toZDT(10), veryClose),
                  'Test 8: Failed to convert time.Duration');

  // Test 9: DecimalType
  console.info("Executing Test" + (++testNum));
  testUtils.assert(time.toZDT(new DecimalType(10)).isClose(time.toZDT(20), veryClose),
                  'Test 9: Failed to convert DecimalType');

  // Test 10: QuantityType
  console.info("Executing Test" + (++testNum));
  testUtils.assert(time.toZDT(new QuantityType('10 s')).isClose(time.toZDT(10000), veryClose),
                  'Test 10: Failed. to convert QuantityType');

  // Test 11: Unsupported units
  console.info("Executing Test" + (++testNum));
  testUtils.assert(exceptionExpected(() => time.toZDT(new QuantityType(10, '°F'))),
                  'Test 11: Failed to throw exception on unsupportted units');

  // Test 12: ISO 8601 String
  // TODO
  console.info("Executing Test" + (++testNum));

  // Test 13: 24 time
  console.info("Executing Test" + (++testNum));
  testUtils.assert(time.toZDT('13:21').isClose(time.toZDT().withHour(13).withMinute(21).withSecond(0).withNano(0), veryClose),
                  'Test 13: Failed to convert 24 hr time without seconds');
  testUtils.assert(time.toZDT('13:21:56').isClose(time.toZDT().withHour(13).withMinute(21).withSecond(56).withNano(0), veryClose),
                  'Test 13: Failed to convert 24 hr time with seconds');
  testUtils.assert(exceptionExpected(() => time.toZDT('25:00:00')),
                  'Test 13: Failed to throw exception for invalid time');

  // Test 14: 12 hr time
  console.info("Executing Test" + (++testNum));
  testUtils.assert(time.toZDT('1:21 pm').isClose(time.toZDT().withHour(13).withMinute(21).withSecond(0).withNano(0), veryClose),
                  'Test 14: Failed to convert 12 hr time without seconds');
  testUtils.assert(time.toZDT('01:21:56pm').isClose(time.toZDT().withHour(13).withMinute(21).withSecond(56).withNano(0), veryClose),
                  'Test 14: Failed to convert 12 hr time with seconds');
  testUtils.assert(exceptionExpected(() => time.toZDT('13:00:00 pm')),
                  'Test 14: Failed to throw exception for invalid time');

  // Test 15: Java ZonedDateTime.toString()
  console.info("Executing Test" + (++testNum));
  now = time.toZDT();
  javaNow = javaZDT.now();
  testUtils.assert(time.toZDT(javaNow.toString()).isClose(now, veryClose),
                  'Test 15: Failed to convert RFC string');

  // Test 16: Java ZonedDateTime.toString()
  console.info("Executing Test" + (++testNum));
  testUtils.assert(time.toZDT('PT10s').isClose(time.toZDT().plusSeconds(10), veryClose),
                  'Test 16: Duration String')

  // Test 17: Unsupported String
  console.info("Executing Test" + (++testNum));
  testUtils.assert(exceptionExpected(() => time.toZDT('unsupported')),
                   'Test 17: Failed to throw exception for unsupportable string');

  // Test 18: Number Item
  console.info("Executing Test" + (++testNum));
  var itemName = prefix+'Number';
  testOhType(itemName, 'Number', '1000');
  testUtils.assert(time.toZDT(items.getItem(itemName)).isClose(time.toZDT(1000), veryClose),
                  'Test 18: Failed to convert Number Item');
  testUtils.assert(time.toZDT(items.getItem(itemName).rawItem).isClose(time.toZDT(1000), veryClose),
                  'Test 18: Failed to convert raw Number Item');

  // Test 19: String Item
  console.info("Executing Test" + (++testNum));
  itemName = prefix+'String';
  testOhType(itemName, 'String', 'PT10S');
  testUtils.assert(time.toZDT(items.getItem(itemName)).isClose(time.toZDT(10000), veryClose),
                  'Test 19: Failed to convert String Item');
  testUtils.assert(time.toZDT(items.getItem(itemName).rawItem).isClose(time.toZDT(10000), veryClose),
                  'Test 19: Failed to convert raw String Item');

  // Test 20: DateTime Item
  console.info("Executing Test" + (++testNum));
  itemName = prefix+'DateTime';
  testOhType(itemName, 'DateTime', new DateTimeType().toString());
  testUtils.assert(time.toZDT(items.getItem(itemName)).isClose(time.toZDT(items.getItem(itemName).rawState), veryClose),
                  'Test 20: Failed to convert DateTimeType');
  testUtils.assert(time.toZDT(items.getItem(itemName).rawItem).isClose(time.toZDT(items.getItem(itemName).rawState), veryClose),
                  'Test 20: Failed to convert raw DateTimeType');

  // Test 21: Number:Time Item
  console.info("Executing Test" + (++testNum));
  itemName = prefix+'Quantity';
  testOhType(itemName, 'Number:Time', '10 s');
  var tdt = time.toZDT(items.getItem(itemName));
  now = time.toZDT('PT10s');
  testUtils.assert(time.toZDT(items.getItem(itemName)).isClose(time.toZDT('PT10s'), veryClose),
                  'Test 21: Failed to convert Number:Time');
  testUtils.assert(time.toZDT(items.getItem(itemName).rawItem).isClose(time.toZDT('PT10s'), veryClose),
                  'Test 21: Failed to convert raw Number:Time');

  // Test 22: Unsupported Item Type
  console.info("Executing Test" + (++testNum));
  itemName = prefix+'Switch';
  testOhType(itemName, 'Switch', 'ON');
  testUtils.assert(exceptionExpected(() => time.toZDT(items.getItem(itemName))),
                  'Test 22: Failed to throw exception for unsupported Item type');
  testUtils.assert(exceptionExpected(() => time.toZDT(items.getItem(itemName).rawItem)),
                  'Test 22: Failed to throw exception for unsupported raw Item type');

  // Test 23: toToday
  console.info("Executing Test" + (++testNum));
  var yesterday = time.toZDT().minusDays(1);
  testUtils.assert(time.toZDT().isClose(yesterday.toToday(), veryClose),
                   'Test 23: Failed to move yesterday to today');

  // Test 24: betweenTimes
  console.info("Executing Test" + (++testNum));
  now = time.toZDT();
  var before = now.withHour(20);
  var start = now.withHour(21);
  var between = now.withHour(22);
  var end = now.withHour(23);
  var after = now.withHour(3);
  testUtils.assert(!before.betweenTimes(start, end), 
                   'Test 24: Failed to find 20 outside between times');
  testUtils.assert(between.betweenTimes(start, end), 
                   'Test 24: Failed to find 22 inside between times');
  end = now.withHour(2);
  testUtils.assert(!before.betweenTimes(start, end), 
                   'Test 24: Failed spans midnight, before times');
  testUtils.assert(!after.betweenTimes(start, end), 
                   'Test 24: Failed spans midnight, after times');  
  testUtils.assert(between.betweenTimes(start, end), 
                   'Test 24: Failed spans midnight, after start before midnight');
  testUtils.assert(now.withHour(1).betweenTimes(start, end), 
                   'Test 24: Failed spans miodnight, before end after midnight');

}
catch(e) {
  console.error(`an error occured: ` + e);
}
finally {
/**
  Items apparently don't get registered, can't remove them
  testItems.forEach(i => {
    console.info('Deleting ' + i);
    items.removeItem(i);
  });
  */
}

console.log("toZDT tests are done");

The functions used from testUtils are:

const assert = (condition, message) => {
  if(!condition) {
    throw new Error(message || 'Assertion failed');
  }
}

const sleep = (msec) => {
  var curr = time.ZonedDateTime.now();
  var done = curr.plus(msec, time.ChronoUnit.MILLIS);
  var timeout = curr.plusSeconds(5);
  while (curr.isBefore(done) && curr.isBefore(timeout)) { // busy wait
    curr = time.ZonedDateTime.now();
  }
  //if(curr.isAfter(timeout)) console.error('sleep timed out!');
}

These are a part of openhab_rules_tools.

rkoshak commented 2 years ago

Second attempt at this capability. I believe I've addressed all the comments from the previous PR and probably introduced all sorts of new ones since it's pretty much a complete rewrite.

I removed some functions as they probably are not that useful overall (toYesterday, toTomorrow) and which I no longer needed because I used a different implementation for betweenTimes. I added a new monkey patched isClose() method that tests the current ZDT and a passed in ZDT to see if they are within a time.Duration apart. I originally wrote it as part of my testing code but decided it's pretty useful in general.

rkoshak commented 2 years ago

I didn't say it which was a mistake.

This is ready for review.

rkoshak commented 2 years ago

The requested changes have been made except for adding back in stuff that shouldn't have been in this PR in the first place.

rkoshak commented 2 years ago

I'm still confused about why it's @memberof time as opposed to @memberof time.ZonedDateTime for the monkey patch functions. I've left them as just @memberof time and removed the tag from the functions that are not exposed.

florian-h05 commented 2 years ago

as opposed to @memberof time.ZonedDateTime for the monkey patch functions

Yes, that would make more sense, but I do not know if that works with the JSDoc htmls generated. You can give it a try and run npm run docs and then have a look at the generated htmls. I think you need to define a new namespace time.ZonedDateTime.

rkoshak commented 2 years ago

Interestingly, neither time nor time.ZonedDateTime generates anything at all for the monkey patched methods. I'm not sure what to do. Since neither work it seems like I should remove them and we'll just have the docs added to the README.MD for the new functions.

florian-h05 commented 2 years ago

Since neither work it seems like I should remove them and we'll just have the docs added to the README.MD for the new functions.

I would‘t remove them as they are at least helpful when scrolling through the source code, but yes, the README seems to be the only place where the monkey-patched functions can be documented (outside the source code).

rkoshak commented 2 years ago

In that case I'd say let's just leave them as is and consider this PR good to go.

digitaldan commented 2 years ago

Some conflicts popped up from an earlier PR that was merged, can you fix those? Thanks!

rkoshak commented 2 years ago

Fixing the conflict was easy for README.MD but it's marking everything I've added to time.js as a conflict. How to I fix this without basically reversing everything I've added? I don't have write access so I can't tell it to use my version instead of main. I'm not sure what to do.

digitaldan commented 2 years ago

I don't have write access so I can't tell it to use my version instead of main. I'm not sure what to do.

Your PR is all that needs to be changed. Fetch the main branch, then in your PR branch do a git merge main or you can use git rebase main . This will result in conflicts and give instructions on how to proceed. From here you remove the lines in the file which show the conflicts like <<<<<<< toZDT , this should be simple as the conflicts are all new inserts, nothing is actually being changed from main (its odd git didn't handle this better), then do a git add time.js and do a commit and push.

digitaldan commented 2 years ago

I went ahead and fixed the conflicts , so all is merged now.

rkoshak commented 2 years ago

Thanks! I thought I wasn't supposed to fetch from main because last time I did that in the toggle PR I ended up with someone else's PR mixed in and it appears to have caused all sorts of problems. Sometimes I miss the simplicity of good old CVS. I feel like a bull in a china shop every time I mess with a PR on Github.

The good news is I think I've figured out Mocha so I'll be able to add some unit tests in a separate PR when I can implement them. I'll look into what it would take to create tests for as much in the library as we can.