Coverage for /Users/davegaeddert/Development/dropseed/plain/plain-models/plain/models/functions/math.py: 81%

103 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-16 22:03 -0500

1from plain.models.expressions import Func, Value 

2from plain.models.fields import FloatField, IntegerField 

3from plain.models.functions import Cast 

4from plain.models.functions.mixins import ( 

5 FixDecimalInputMixin, 

6 NumericOutputFieldMixin, 

7) 

8from plain.models.lookups import Transform 

9 

10 

11class Abs(Transform): 

12 function = "ABS" 

13 lookup_name = "abs" 

14 

15 

16class ACos(NumericOutputFieldMixin, Transform): 

17 function = "ACOS" 

18 lookup_name = "acos" 

19 

20 

21class ASin(NumericOutputFieldMixin, Transform): 

22 function = "ASIN" 

23 lookup_name = "asin" 

24 

25 

26class ATan(NumericOutputFieldMixin, Transform): 

27 function = "ATAN" 

28 lookup_name = "atan" 

29 

30 

31class ATan2(NumericOutputFieldMixin, Func): 

32 function = "ATAN2" 

33 arity = 2 

34 

35 def as_sqlite(self, compiler, connection, **extra_context): 

36 if not getattr( 

37 connection.ops, "spatialite", False 

38 ) or connection.ops.spatial_version >= (5, 0, 0): 

39 return self.as_sql(compiler, connection) 

40 # This function is usually ATan2(y, x), returning the inverse tangent 

41 # of y / x, but it's ATan2(x, y) on SpatiaLite < 5.0.0. 

42 # Cast integers to float to avoid inconsistent/buggy behavior if the 

43 # arguments are mixed between integer and float or decimal. 

44 # https://www.gaia-gis.it/fossil/libspatialite/tktview?name=0f72cca3a2 

45 clone = self.copy() 

46 clone.set_source_expressions( 

47 [ 

48 Cast(expression, FloatField()) 

49 if isinstance(expression.output_field, IntegerField) 

50 else expression 

51 for expression in self.get_source_expressions()[::-1] 

52 ] 

53 ) 

54 return clone.as_sql(compiler, connection, **extra_context) 

55 

56 

57class Ceil(Transform): 

58 function = "CEILING" 

59 lookup_name = "ceil" 

60 

61 

62class Cos(NumericOutputFieldMixin, Transform): 

63 function = "COS" 

64 lookup_name = "cos" 

65 

66 

67class Cot(NumericOutputFieldMixin, Transform): 

68 function = "COT" 

69 lookup_name = "cot" 

70 

71 

72class Degrees(NumericOutputFieldMixin, Transform): 

73 function = "DEGREES" 

74 lookup_name = "degrees" 

75 

76 

77class Exp(NumericOutputFieldMixin, Transform): 

78 function = "EXP" 

79 lookup_name = "exp" 

80 

81 

82class Floor(Transform): 

83 function = "FLOOR" 

84 lookup_name = "floor" 

85 

86 

87class Ln(NumericOutputFieldMixin, Transform): 

88 function = "LN" 

89 lookup_name = "ln" 

90 

91 

92class Log(FixDecimalInputMixin, NumericOutputFieldMixin, Func): 

93 function = "LOG" 

94 arity = 2 

95 

96 def as_sqlite(self, compiler, connection, **extra_context): 

97 if not getattr(connection.ops, "spatialite", False): 

98 return self.as_sql(compiler, connection) 

99 # This function is usually Log(b, x) returning the logarithm of x to 

100 # the base b, but on SpatiaLite it's Log(x, b). 

101 clone = self.copy() 

102 clone.set_source_expressions(self.get_source_expressions()[::-1]) 

103 return clone.as_sql(compiler, connection, **extra_context) 

104 

105 

106class Mod(FixDecimalInputMixin, NumericOutputFieldMixin, Func): 

107 function = "MOD" 

108 arity = 2 

109 

110 

111class Pi(NumericOutputFieldMixin, Func): 

112 function = "PI" 

113 arity = 0 

114 

115 

116class Power(NumericOutputFieldMixin, Func): 

117 function = "POWER" 

118 arity = 2 

119 

120 

121class Radians(NumericOutputFieldMixin, Transform): 

122 function = "RADIANS" 

123 lookup_name = "radians" 

124 

125 

126class Random(NumericOutputFieldMixin, Func): 

127 function = "RANDOM" 

128 arity = 0 

129 

130 def as_mysql(self, compiler, connection, **extra_context): 

131 return super().as_sql(compiler, connection, function="RAND", **extra_context) 

132 

133 def as_sqlite(self, compiler, connection, **extra_context): 

134 return super().as_sql(compiler, connection, function="RAND", **extra_context) 

135 

136 def get_group_by_cols(self): 

137 return [] 

138 

139 

140class Round(FixDecimalInputMixin, Transform): 

141 function = "ROUND" 

142 lookup_name = "round" 

143 arity = None # Override Transform's arity=1 to enable passing precision. 

144 

145 def __init__(self, expression, precision=0, **extra): 

146 super().__init__(expression, precision, **extra) 

147 

148 def as_sqlite(self, compiler, connection, **extra_context): 

149 precision = self.get_source_expressions()[1] 

150 if isinstance(precision, Value) and precision.value < 0: 

151 raise ValueError("SQLite does not support negative precision.") 

152 return super().as_sqlite(compiler, connection, **extra_context) 

153 

154 def _resolve_output_field(self): 

155 source = self.get_source_expressions()[0] 

156 return source.output_field 

157 

158 

159class Sign(Transform): 

160 function = "SIGN" 

161 lookup_name = "sign" 

162 

163 

164class Sin(NumericOutputFieldMixin, Transform): 

165 function = "SIN" 

166 lookup_name = "sin" 

167 

168 

169class Sqrt(NumericOutputFieldMixin, Transform): 

170 function = "SQRT" 

171 lookup_name = "sqrt" 

172 

173 

174class Tan(NumericOutputFieldMixin, Transform): 

175 function = "TAN" 

176 lookup_name = "tan"