# Copyright (c) 2004-2005 DoCoMo Euro-Labs GmbH (Munich, Germany). # Copyright (c) 2001-2005 LOGILAB S.A. (Paris, FRANCE). # # http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """narval IM presence manipulation's related actions :version: $Revision:$ :author: Logilab :copyright: 2000-2005 LOGILAB S.A. (Paris, FRANCE) 2004-2005 DoCoMo Euro-Labs GmbH (Munich, Germany) :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com """ __revision__ = '$Id:$' __docformat__ = 'restructuredtext en' import sys from time import mktime, localtime # days of week index in a local time tuple DOWIDX = 6 class Away: """class representing a time slot where some jabber user has been away """ def __init__(self, start, end_time=None): self.status = start['status'] self.show = start['show'] self.start_time = start['time'] self.duration = 0 self.end_time = None if end_time is not None: self.back(end_time) def __repr__(self): return "Away({'status': %(status)r, 'show': %(show)r, 'time': \ %(start_time)r}, end_time=%(end_time)r)" % self.__dict__ def __eq__(self, other): """for test purpose""" return (other.status == self.status and other.show == self.show and other.start_time == self.start_time and other.end_time == self.end_time and other.duration == self.duration) def __ne__(self, other): return not self == other def back(self, end_time): """complete data with user's back time""" assert end_time > self.start_time self.end_time = end_time self.duration = mktime(self.end_time) - mktime(self.start_time) def is_available(pres_info): """return True if the presence information means the user is available """ return (not pres_info['show'] and pres_info['status'].lower() != 'disconnected') def away_from_presences(presences): """return a generator on Away instances from an ordered list of raw presence information """ current = None for pres_info in presences: if current is None: # waiting for an unavailable presence if not is_available(pres_info): current = Away(pres_info) else: print >> sys.stderr, 'skipping presence', pres_info, \ '(waiting for a show="something" element)' else: # waiting for an available presence if is_available(pres_info): current.back(pres_info['time']) yield current current = None else: current.back(pres_info['time']) yield current current = Away(pres_info) def similarity_factor(away_ref, away): """return a similarity factor between a reference presence information dict and another (usually extracted from a pre-recorded data set) the similarity factor is a float value between 0 and 2 """ if away_ref.show == away.show \ and away_ref.status == away.status: return 2. datetime_ref, datetime = away_ref.start_time, away.start_time factor = 0. if away_ref.show == away.show: factor += 0.2 if away_ref.status == away.status: factor += 0.4 if datetime_ref[DOWIDX] == datetime[DOWIDX]: factor += 0.4 minutes_ref, minutes = mktime(datetime_ref) / 60, mktime(datetime) / 60 if (minutes - 30) <= minutes_ref <= (minutes + 30): factor += 0.6 return factor class PresenceAnalysisError(Exception): pass def expected_time_back(presences, minimum=None): """return a local time tuple indicating the expected time where the user whose presence data come from will be back """ pres_info_ref = presences[-1] if is_available(pres_info_ref): raise PresenceAnalysisError('%s is present') away_ref = Away(pres_info_ref) expected_duration = 0 diviser = 0 for away in away_from_presences(presences): if not minimum or (minimum and away.duration > minimum): factor = similarity_factor(away_ref, away) expected_duration += away.duration * factor diviser += factor try: expected_duration = expected_duration / diviser except ZeroDivisionError: raise PresenceAnalysisError('no data for %s') # FIXME: variance ? return localtime(mktime(away_ref.start_time) + expected_duration) def extrapol_time_back(presences, elapsed_period): """return a local time tuple indicating the extrapolated time where the user whose presence data come from will be back """ return expected_time_back(presences, elapsed_period) import unittest from logilab.common import testlib TEST_DATA = [{'status': 'un peu de sport', 'time': (2005, 1, 24, 18, 3, 21, 0, 24, 0), 'show': 'away'}, {'status': '', 'show': '', 'time': (2005, 1, 25, 9, 30, 18, 1, 25, 0)}, {'status': 'Sorry, I ran out for a bit!', 'show': 'away', 'time': (2005, 1, 25, 9, 31, 41, 1, 25, 0)}, {'status': '', 'show': '', 'time': (2005, 1, 25, 9, 33, 32, 1, 25, 0)}, {'status': 'Sorry, I ran out for a bit!', 'show': 'away', 'time': (2005, 1, 25, 9, 37, 21, 1, 25, 0)}, {'status': '', 'show': '', 'time': (2005, 1, 25, 9, 41, 1, 1, 25, 0)}, {'status': 'Sorry, I ran out for a bit!', 'show': 'away', 'time': (2005, 1, 25, 9, 43, 41, 1, 25, 0)}, {'status': '', 'show': '', 'time': (2005, 1, 25, 10, 12, 20, 1, 25, 0)}, {'status': 'Sorry, I ran out for a bit!', 'show': 'away', 'time': (2005, 1, 25, 10, 18, 21, 1, 25, 0)}, {'status': '', 'show': '', 'time': (2005, 1, 25, 11, 14, 20, 1, 25, 0)}, {'status': "c'est la pause", 'show': 'away', 'time': (2005, 1, 25, 11, 33, 1, 1, 25, 0)}, {'status': '', 'show': '', 'time': (2005, 1, 25, 11, 36, 27, 1, 25, 0)}, {'status': "c'est la pause", 'show': 'away', 'time': (2005, 1, 25, 12, 5, 1, 1, 25, 0)}, {'status': '', 'show': '', 'time': (2005, 1, 25, 12, 12, 9, 1, 25, 0)}, {'status': "c'est la pause", 'show': 'away', 'time': (2005, 1, 25, 12, 23, 21, 1, 25, 0)}, {'status': '', 'show': '', 'time': (2005, 1, 25, 13, 51, 7, 1, 25, 0)}, {'status': "c'est la pause", 'show': 'away', 'time': (2005, 1, 25, 15, 56, 21, 1, 25, 0)} ] class ExpectedTimeBackTC(unittest.TestCase): def test(self): self.assertEquals(expected_time_back(TEST_DATA), (2005, 1, 25, 16, 14, 38, 1, 25, 0)) class AwayFromPresencesTC(testlib.TestCase): def test(self): self.assertListEquals(list(away_from_presences(TEST_DATA)), [Away({'status': 'un peu de sport', 'show': 'away', 'time': (2005, 1, 24, 18, 3, 21, 0, 24, 0)}, end_time=(2005, 1, 25, 9, 30, 18, 1, 25, 0)), Away({'status': 'Sorry, I ran out for a bit!', 'show': 'away', 'time': (2005, 1, 25, 9, 31, 41, 1, 25, 0)}, end_time=(2005, 1, 25, 9, 33, 32, 1, 25, 0)), Away({'status': 'Sorry, I ran out for a bit!', 'show': 'away', 'time': (2005, 1, 25, 9, 37, 21, 1, 25, 0)}, end_time=(2005, 1, 25, 9, 41, 1, 1, 25, 0)), Away({'status': 'Sorry, I ran out for a bit!', 'show': 'away', 'time': (2005, 1, 25, 9, 43, 41, 1, 25, 0)}, end_time=(2005, 1, 25, 10, 12, 20, 1, 25, 0)), Away({'status': 'Sorry, I ran out for a bit!', 'show': 'away', 'time': (2005, 1, 25, 10, 18, 21, 1, 25, 0)}, end_time=(2005, 1, 25, 11, 14, 20, 1, 25, 0)), Away({'status': "c'est la pause", 'show': 'away', 'time': (2005, 1, 25, 11, 33, 1, 1, 25, 0)}, end_time=(2005, 1, 25, 11, 36, 27, 1, 25, 0)), Away({'status': "c'est la pause", 'show': 'away', 'time': (2005, 1, 25, 12, 5, 1, 1, 25, 0)}, end_time=(2005, 1, 25, 12, 12, 9, 1, 25, 0)), Away({'status': "c'est la pause", 'show': 'away', 'time': (2005, 1, 25, 12, 23, 21, 1, 25, 0)}, end_time=(2005, 1, 25, 13, 51, 7, 1, 25, 0))]) class SimilarityFactorTC(unittest.TestCase): def test_identical(self): d = Away({'status': '', 'show': '', 'time': (2005, 1, 25, 15, 55, 17, 1, 25, 0)}) self.assertEquals(similarity_factor(d, d), 1) def test_diff_status(self): d1 = Away({'status': '', 'show': '', 'time': (2005, 1, 25, 15, 55, 17, 1, 25, 0)}) d2 = Away({'status': 'autre chose', 'show': '', 'time': (2005, 1, 25, 15, 55, 17, 1, 25, 0)}) self.assertAlmostEquals(similarity_factor(d1, d2), 0.7) def test_diff_show(self): d1 = Away({'status': '', 'show': '', 'time': (2005, 1, 25, 15, 55, 17, 1, 25, 0)}) d2 = Away({'status': '', 'show': 'bla', 'time': (2005, 1, 25, 15, 55, 17, 1, 25, 0)}) self.assertEquals(similarity_factor(d1, d2), 0.8) def test_time(self): d1 = Away({'status': '', 'show': 'bla', 'time': (2005, 1, 25, 15, 55, 17, 1, 25, 0)}) d2 = Away({'status': 'bla', 'show': '', 'time': (2005, 1, 25, 15, 25, 17, 1, 25, 0)}) self.assertEquals(similarity_factor(d1, d2), 0.5) d1 = Away({'status': '', 'show': 'bla', 'time': (2005, 1, 25, 15, 55, 17, 1, 25, 0)}) d2 = Away({'status': 'bla', 'show': '', 'time': (2005, 1, 25, 16, 5, 17, 1, 25, 0)}) self.assertEquals(similarity_factor(d1, d2), 0.5) d1 = Away({'status': '', 'show': 'bla', 'time': (2005, 1, 25, 15, 55, 17, 1, 25, 0)}) d2 = Away({'status': 'bla', 'show': '', 'time': (2005, 1, 25, 15, 5, 17, 1, 25, 0)}) self.assertEquals(similarity_factor(d1, d2), 0.2) def test_day_of_week(self): d1 = Away({'status': '', 'show': 'bla', 'time': (2005, 1, 25, 15, 55, 17, 1, 25, 0)}) d2 = Away({'status': 'bla', 'show': '', 'time': (2005, 1, 25, 15, 26, 17, 1, 25, 0)}) self.assertEquals(similarity_factor(d1, d2), 0.5) d1 = Away({'status': '', 'show': 'bla', 'time': (2005, 1, 25, 15, 55, 17, 1, 25, 0)}) d2 = Away({'status': 'bla', 'show': '', 'time': (2005, 1, 25, 15, 26, 17, 2, 25, 0)}) self.assertEquals(similarity_factor(d1, d2), 0.3) if __name__ == '__main__': unittest.main()